├── .gitignore ├── .gitmodules ├── .npmignore ├── CHANGES.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── bin ├── fastbench ├── fastcall ├── fastclatency ├── fastclatencytime ├── fastcsnoop ├── fastserve ├── fastslatency └── fastssnoop ├── lib ├── bench.js ├── demo_server.js ├── fast.js ├── fast_client.js ├── fast_client_request.js ├── fast_protocol.js ├── fast_server.js └── subr.js ├── package-lock.json ├── package.json ├── test ├── common.js ├── common │ └── client.js ├── compat │ ├── Makefile.compat.defs │ ├── Makefile.compat.targ │ ├── common.js │ ├── legacy-server.js │ ├── manual-tst.client_compat.js │ └── setup.js ├── tst.allocator.js ├── tst.client_generic.js ├── tst.client_request.js ├── tst.protocol_decoder.js ├── tst.protocol_encoder.js ├── tst.server.js └── tst.socket_summarize.js └── tools ├── bashstyle ├── jsl.node.conf ├── mk ├── Makefile.deps └── Makefile.targ └── perfrun /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/compat/node_modules 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/catest"] 2 | path = deps/catest 3 | url = https://github.com/joyent/catest 4 | [submodule "deps/jsstyle"] 5 | path = deps/jsstyle 6 | url = https://github.com/davepacheco/jsstyle.git 7 | [submodule "deps/javascriptlint"] 8 | path = deps/javascriptlint 9 | url = https://github.com/davepacheco/javascriptlint.git 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | deps 2 | Makefile 3 | Makefile.targ 4 | test 5 | tools 6 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Fast Changelog 2 | 3 | ## Not yet released. 4 | 5 | None yet. 6 | 7 | ## v3.1.2 8 | 9 | * Bump microtime to 3.0.0 for compatibility with node v6+ 10 | 11 | ## v3.1.1 12 | 13 | * Added fsr_context.removeSocketEndListener() method so that long running RPC 14 | calls can remove their listener. 15 | 16 | ## v3.1.0 17 | 18 | * Added fsr_context.addSocketEndListener() method so that long running RPC 19 | calls can tell when the client socket has ended. 20 | 21 | ## v3.0.1 22 | 23 | * Add support for a `metricLabels` option when creating node-fast clients. This 24 | option must be a valid [artedi](https://github.com/joyent/node-artedi) labels 25 | object. 26 | 27 | ## v3.0.0 28 | 29 | * Bump the protocol version to 2 as a means to better deal with the CRC 30 | incompatibility stemming from the use of node-crc version 0.3.0. This change 31 | replaces the attempt at addressing this issue that was part of v2.8.1. That 32 | solution was found to be insufficient since there were cases where the 33 | calcluated CRC codes from the 0.3.0 version of node-crc were correct. With 34 | this release the protocol version is used to clearly determine which version 35 | of node-crc should be used to decode a received message and encode a 36 | response. Servers using this version are still compatible with fast clients 37 | using protocol version 1, but servers using protocol version 1 will not 38 | support clients using protocol version 2. 39 | 40 | ## v2.8.1 41 | 42 | * node-fast use wrong CRC library version when encoding errors to clients using 43 | updated CRC library version 44 | 45 | ## v2.8.0 46 | 47 | * node-fast provider transitive dependencies too old to build on x86_64 48 | 49 | ## v2.7.0 50 | 51 | * #23 Add support for updated crc package to improve interoperability 52 | 53 | ## v2.6.1 54 | 55 | * #22 Fix copy-paste bug in fast_server module 56 | 57 | ## v2.6.0 58 | 59 | * `fast_client_request_time_ms` became `fast_client_request_time_seconds` and is 60 | now measured in seconds rather than milliseconds. 61 | * `fast_request_time_ms` became `fast_server_request_time_seconds` and is 62 | now measured in seconds rather than milliseconds. 63 | * Add support for artedi v2 which requires histograms to use fixed buckets. 64 | When the collector passed is an artedi v2 collector, default buckets are 65 | generated via artedi.logLinearBuckets(10, -1, 3, 5). See: joyent/node-artedi#17 66 | for more details. Data generated with artedi v1 collectors are likely 67 | invalid (and have been with previous releases as well). Dashboards/queries for 68 | services which pass v2 collectors to node-fast, should restrict results using 69 | the `{buckets_version="1"}` label when performing Prometheus queries on this 70 | data. 71 | 72 | ## v2.5.0 73 | 74 | * #17 node-fast could track client metrics 75 | * #16 fastserve cannot be run concurrently even with different arguments 76 | 77 | ## v2.4.0 78 | 79 | * #14 want node-fast FastServer 'onConnsDestroyed' method 80 | 81 | ## v2.3.2 82 | 83 | * #15 server leaks memory on socket errors 84 | * #12 want support for node v6.12.0 85 | 86 | ## v2.3.1 87 | 88 | * #11 add RPC method name to metrics 89 | 90 | ## v2.3.0 91 | 92 | * #9 node-fast could track basic request metrics 93 | 94 | ## v2.2.4 95 | 96 | * #7 fsr_context isn't always unpiped from fsr_encoder, which causes memory leaks 97 | 98 | ## v2.2.3 99 | 100 | * #5 Server shutdown crashes when a connection had a socket error 101 | 102 | ## v2.2.2 103 | 104 | * #4 client should preserve "ase\_errors" for Errors 105 | 106 | ## v2.2.1 107 | 108 | * #3 client should not log at "error" level 109 | 110 | ## v2.2.0 111 | 112 | * #2 update dependencies to support Node v4 113 | 114 | ## v2.1.0 115 | 116 | * #1 need limited support for servers that send null values 117 | 118 | ## v2.0.0 (2016-08-29) 119 | 120 | This is a complete rewrite of the Fast client and server implementation. Almost 121 | everything about the API has changed, including constructors, methods, and 122 | arguments. The primary difference is that the new Fast client is generally only 123 | responsible for Fast-protocol-level concerns. It does not do service discovery 124 | (via DNS), connection establishment, health monitoring, retries, or anything 125 | like that. Callers are expected to use something like node-cueball for that. 126 | The server manages a little bit more of the TCP state than the client does 127 | (particularly as clients connect), but callers are still expected to manage the 128 | server socket itself. To summarize: callers are expected to manage their own 129 | `net.Socket` (for clients) or `net.Server` (for servers), watching those objects 130 | for whatever events they're interested in and using those classes' methods for 131 | working with them. You no longer treat the Fast client as a wrapper for a 132 | `net.Socket` or the Fast server as a wrapper for a `net.Server`. 133 | 134 | As a result of this change in design, the constructor arguments are pretty 135 | different. Many methods are gone (e.g., client.connect(), client.close(), 136 | server.listen(), server.address(), and so on). The interface is generally much 137 | simpler. 138 | 139 | To make methods more extensible and consistent with Joyent's Best Practices for 140 | Error Handling (see README), the major APIs (constructors, `client.rpc()`, and 141 | `server.rpc()`) have been changed to accept named arguments in a single `args` 142 | argument. 143 | 144 | Other concrete API differences include: 145 | 146 | * Clients that make RPC calls get back a proper Readable stream, rather than an 147 | EventEmitter that emits `message`, `end`, and `error` events. (The previous 148 | implementation used the spirit of Node.js's object-mode streams, but predated 149 | their maturity, and so implemented something similar but ad-hoc.) As a result 150 | of this change, you cannot send `null` over the RPC channel, since object-mode 151 | streams use `null` to denote end-of-stream. Of course, you can wrap `null` in 152 | an object. 153 | * `Client.rpc()` now takes named arguments as mentioned above. 154 | * `Server.rpc()` is now called `Server.registerRpcMethod()` and takes named 155 | arguments as mentioned above. 156 | * The server no longer provides an `after` event after RPC requests complete. 157 | 158 | Other notable differences include: 159 | 160 | * This implementation fixes a large number of protocol bugs, including cases 161 | where the protocol version was ignored in incoming messages and unsupported 162 | kinds of messages were treated as new RPC requests. 163 | * This implementation does not support aborting RPC requests because the 164 | previous mechanism for this was somewhat dangerous. (See README.) 165 | * The client DTrace probes have been revised. Server DTrace probes have been 166 | added. (See README.) 167 | * Kang entry points have been added for observability. (See README.) 168 | 169 | The original implementation can be found in the [v0.x 170 | branch](https://github.com/joyent/node-fast/tree/fast-v0.x). 171 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to node-fast 2 | 3 | This repository uses GitHub pull requests for code review. 4 | 5 | See the [Joyent Engineering 6 | Guidelines](https://github.com/joyent/eng/blob/master/docs/index.md) for general 7 | best practices expected in this repository. 8 | 9 | Contributions should be "make prepush" clean. The "prepush" target runs the 10 | "check" target, which requires these separate tools: 11 | 12 | * https://github.com/davepacheco/jsstyle 13 | * https://github.com/davepacheco/javascriptlint 14 | 15 | If you're changing something non-trivial or user-facing, you may want to submit 16 | an issue first. Because this protocol is widely deployed, we generally view 17 | backwards compatibility (both on the wire and in the API) as a constraint on all 18 | changes. 19 | 20 | 21 | ## Testing 22 | 23 | Automated tests can be run using `make test`. This test suite should be pretty 24 | exhaustive for both basic functionality and edge cases. 25 | 26 | You can use the `fastserve` and `fastcall` programs for ad-hoc testing. 27 | 28 | 29 | ## Backwards compatibility with older versions of Fast 30 | 31 | As mentioned in the README, there was a previous implementation of this 32 | protocol. The client and server APIs are different in this module than the 33 | previous implementation. However, the client here is compatible with a server 34 | built on the previous implementation, and the server here is compatible with a 35 | client built on the previous implementation. 36 | 37 | There are several reasons we might want to use the old implementation in this 38 | module: 39 | 40 | * done: basic functionality testing this client against the old server 41 | * not implemented: basic functionality testing this server against the old 42 | client 43 | * not implemented: for performance testing either the old client or server 44 | 45 | This is made significantly more complicated by the fact that the old 46 | implementation only works with Node 0.10, while this implementation is expected 47 | to run on Node 0.12 and later. 48 | 49 | **The easiest way to test compatibility is to set `FAST\_COMPAT\_NODEDIR` in 50 | your environment and run `make test-compat`**: 51 | 52 | export FAST_COMPAT_NODEDIR=/path/to/node-v0.10-directory 53 | make test-compat 54 | 55 | This will do the following: 56 | 57 | * sanity-check the `node` in `$FAST_COMPAT_NODEDIR/bin/node` 58 | * use the `npm` in that directory to install the _old_ fast module into 59 | `test/compat/node_modules/fast` 60 | * run the compatibility tests in `test/compat`. 61 | 62 | These could be incorporated into "make prepush", but developers would have to 63 | have `FAST_COMPAT_NODEDIR` set to a directory containing 0.10. 64 | 65 | 66 | ## Performance testing 67 | 68 | The `fastbench` command can be used to make RPC requests to a remote server 69 | for a fixed number of requests, for a given period of time, or until the program 70 | itself is killed. The tool reports very coarse metrics about client-side 71 | request performance: 72 | 73 | $ fastbench -c 10 sync 127.0.0.1 8123 74 | pid 17354: running workload "sync" until killed (type CTRL-C for results) 75 | established connection to 127.0.0.1:8123 76 | ^Cstopping due to SIGINT 77 | ----------------------------------- 78 | total runtime: 4.233982s 79 | total requests completed: 6063 (6073 issued) 80 | total unexpected errors: 0 81 | error rate: 0.00% 82 | maximum concurrency: 10 83 | request throughput: 1431 requests per second 84 | estimated average latency: 6983 us 85 | 86 | The server must support the "fastbench" RPC method, which basically echoes its 87 | arguments and optionally sleeps. The `fastserve` command implements this RPC, 88 | as does the legacy server in ./test/compat that's described above. 89 | 90 | This should be considered only a rough estimate of performance under very 91 | particular conditions. Proper performance analysis requires not just careful 92 | collection of data, but also understanding exactly what the system is doing and 93 | where the bottleneck is to make sure you're looking at correct numbers. 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Mark Cavage, All rights reserved. 2 | Copyright (c) 2016, Joyent, Inc. All rights reserved. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2016, Joyent, Inc. All rights reserved. 3 | # 4 | # Makefile: top-level Makefile 5 | # 6 | # This Makefile contains only repo-specific logic and uses included makefiles 7 | # to supply common targets (javascriptlint, jsstyle, restdown, etc.), which are 8 | # used by other repos as well. 9 | # 10 | 11 | # 12 | # Tools 13 | # 14 | CATEST = deps/catest/catest 15 | NPM = npm 16 | NODE = node 17 | 18 | # 19 | # Files 20 | # 21 | JSON_FILES = package.json 22 | BASH_FILES = $(wildcard test/*.sh) 23 | JS_FILES := bin/fastbench \ 24 | bin/fastcall \ 25 | bin/fastserve \ 26 | $(shell find lib test -name '*.js' | \ 27 | grep -v ^test/compat/node_modules) 28 | CATEST_FILES = $(shell find test -name 'tst.*.js' | \ 29 | grep -v ^test/compat/node_modules) 30 | JSL_FILES_NODE = $(JS_FILES) 31 | JSSTYLE_FILES = $(JS_FILES) 32 | JSL_CONF_NODE = tools/jsl.node.conf 33 | 34 | include ./test/compat/Makefile.compat.defs 35 | 36 | .PHONY: all 37 | all: 38 | $(NPM) install 39 | CLEAN_FILES += node_modules 40 | 41 | .PHONY: test 42 | test: | $(CATEST) 43 | $(CATEST) $(CATEST_FILES) 44 | @echo Note: Compatibility tests need to be run manually with \ 45 | \"make test-compat\". 46 | 47 | $(CATEST): deps/catest/.git 48 | 49 | include ./tools/mk/Makefile.deps 50 | include ./tools/mk/Makefile.targ 51 | include ./test/compat/Makefile.compat.targ 52 | -------------------------------------------------------------------------------- /bin/fastbench: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* vim: set ft=javascript: */ 3 | /* 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | */ 8 | 9 | /* 10 | * Copyright 2020 Joyent, Inc. 11 | */ 12 | 13 | /* 14 | * fastbench: command-line tool for making a large number of RPC requests and 15 | * measuring latency and throughput. 16 | */ 17 | 18 | var mod_artedi = require('artedi'); 19 | var mod_assertplus = require('assert-plus'); 20 | var mod_bunyan = require('bunyan'); 21 | var mod_cmdutil = require('cmdutil'); 22 | var mod_extsprintf = require('extsprintf'); 23 | var mod_getopt = require('posix-getopt'); 24 | var mod_http = require('http'); 25 | var mod_jsprim = require('jsprim'); 26 | var mod_kang = require('kang'); 27 | var mod_net = require('net'); 28 | var mod_os = require('os'); 29 | var mod_strsplit = require('strsplit'); 30 | var mod_vasync = require('vasync'); 31 | 32 | var printf = mod_extsprintf.printf; 33 | var sprintf = mod_extsprintf.sprintf; 34 | var VError = require('verror'); 35 | 36 | var mod_fast = require('../lib/fast'); 37 | var mod_fastbench = require('../lib/bench'); 38 | 39 | /* default concurrency of the client */ 40 | var fbDflConcurrency = 1; 41 | /* default reporting interval, in seconds */ 42 | var fbDflReportingInterval = 5; 43 | /* default port for kang server */ 44 | var fbDflKangPort = 16520; 45 | /* default port for artedi server */ 46 | var fbDflArtediPort = 16521; 47 | 48 | /* 49 | * The Big Theory Statement in lib/fast_server.js explains that completing N 50 | * requests over a single client socket takes O(N^2) time. To keep this 51 | * manageable, fastbench attempts to maintain a fixed number of requests per 52 | * connection. 53 | */ 54 | var fbDflRequestsPerConnection = 10; 55 | 56 | function fbUsageMessage() 57 | { 58 | var msg, workloads; 59 | 60 | msg = 'Runs Fast RPC requests to measure latency and throughput.\n\n'; 61 | msg += 'Available canned workloads:\n\n'; 62 | workloads = mod_fastbench.fastBenchWorkloads(); 63 | mod_jsprim.forEachKey(workloads, function (name, workload) { 64 | msg += sprintf(' %-8s %s\n', name, workload.description); 65 | }); 66 | 67 | msg += [ 68 | '', 69 | 'Options:', 70 | '', 71 | ' -c | --concurrency N Allow up to N outstanding ' + 72 | 'requests at once', 73 | ' (default: ' + 74 | fbDflConcurrency + ')', 75 | ' -d | --duration NSECONDS Stop after NSECONDS seconds.', 76 | ' -i | --interval NSECONDS Report every NSECONDS seconds.', 77 | ' -k | --kang-port PORT Use PORT for kang endpoint.', 78 | ' -n | --nrequests NREQUESTS Stop after completing ' + 79 | 'NREQUESTS requests.', 80 | ' -p | --artedi-port PORT Use PORT for metrics endpoint.', 81 | '' 82 | ].join('\n'); 83 | 84 | return (msg); 85 | } 86 | 87 | function main() 88 | { 89 | var parser, option, args; 90 | var workloads, wname; 91 | var config = { 92 | 'fbHost': null, 93 | 'fbPort': null, 94 | 'fbWorkload': null, 95 | 'fbNRequests': null, 96 | 'fbDuration': null, 97 | 'fbReportingInterval': fbDflReportingInterval * 1000, 98 | 'fbConcurrency': fbDflConcurrency, 99 | 'fbArtediPort': fbDflArtediPort, 100 | 'fbKangPort': fbDflKangPort 101 | }; 102 | 103 | mod_cmdutil.configure({ 104 | 'synopses': [ 105 | '[OPTIONS] WORKLOAD HOST PORT' 106 | ], 107 | 'usageMessage': fbUsageMessage() 108 | }); 109 | 110 | parser = new mod_getopt.BasicParser( 111 | 'c:(count)d:(duration)i:(interval)n:(nrequests)' + 112 | 'p:(artedi-port)k:(kang-port)', 113 | process.argv); 114 | while ((option = parser.getopt()) !== undefined) { 115 | switch (option.option) { 116 | case 'c': 117 | config.fbConcurrency = parseOptInt(option, 1); 118 | break; 119 | 120 | case 'd': 121 | config.fbDuration = 1000 * parseOptInt(option, 1); 122 | break; 123 | 124 | case 'i': 125 | config.fbReportingInterval = 126 | 1000 * parseOptInt(option, 0); 127 | break; 128 | 129 | case 'n': 130 | config.fbNRequests = parseOptInt(option, 1); 131 | break; 132 | 133 | case 'p': 134 | config.fbArtediPort = parseOptInt(option, 1); 135 | break; 136 | 137 | case 'k': 138 | config.fbKangPort = parseOptInt(option, 1); 139 | break; 140 | 141 | default: 142 | /* error message already emitted */ 143 | mod_assertplus.equal('?', option.option); 144 | mod_cmdutil.usage(); 145 | break; 146 | } 147 | } 148 | 149 | args = process.argv.slice(parser.optind()); 150 | if (args.length < 3) { 151 | mod_cmdutil.usage('expected workload, host, and port'); 152 | } 153 | 154 | workloads = mod_fastbench.fastBenchWorkloads(); 155 | wname = args[0]; 156 | if (!workloads.hasOwnProperty(wname)) { 157 | mod_cmdutil.usage('unsupported workload: %s', wname); 158 | } 159 | 160 | config.fbWorkload = new (workloads[wname].cons)(); 161 | config.fbHost = args[1]; 162 | config.fbPort = parseInt(args[2], 10); 163 | if (isNaN(config.fbPort) || config.fbPort < 1 || 164 | config.fbPort > 65535) { 165 | mod_cmdutil.usage('bad port number: %s', args[2]); 166 | } 167 | 168 | fastbench(config); 169 | } 170 | 171 | function parseOptInt(option, min) 172 | { 173 | var v; 174 | 175 | v = parseInt(option.optarg, 10); 176 | if (isNaN(v)) { 177 | mod_cmdutil.usage(new VError( 178 | 'value for -%s is not an integer: "%s"', 179 | option.option, option.optarg)); 180 | } 181 | 182 | mod_assertplus.optionalNumber(min, 'min'); 183 | mod_assertplus.ok(min !== undefined); 184 | if (min !== null && v < min) { 185 | mod_cmdutil.usage(new VError( 186 | 'value for -%s is too small: "%s"', 187 | option.option, option.optarg)); 188 | } 189 | 190 | return (v); 191 | } 192 | 193 | function fastbench(config) 194 | { 195 | var collector; 196 | var log, queue, bencher, nconns, i; 197 | var start = new Date(); 198 | 199 | collector = mod_artedi.createCollector(); 200 | 201 | printf('%s (%d) pid %d: running workload "%s" ', 202 | start.toISOString(), start.getTime(), process.pid, 203 | config.fbWorkload.name()); 204 | 205 | if (config.fbNRequests !== null) { 206 | if (config.fbDuration !== null) { 207 | printf('for %d requests or %d milliseconds', 208 | config.fbNRequests, config.fbDuration); 209 | } else { 210 | printf('for %d requests', config.fbNRequests); 211 | } 212 | } else if (config.fbDuration !== null) { 213 | printf('for %d milliseconds', config.fbDuration); 214 | } else { 215 | printf('until killed (type CTRL-C for results)'); 216 | } 217 | 218 | printf('\n'); 219 | 220 | log = new mod_bunyan({ 221 | 'name': 'fastbench', 222 | 'level': process.env['LOG_LEVEL'] || 'fatal' 223 | }); 224 | 225 | queue = mod_vasync.queuev({ 226 | 'concurrency': 10, 227 | 'worker': function fastbenchInitConn(_, queuecallback) { 228 | var csock; 229 | csock = mod_net.createConnection(config.fbPort, config.fbHost); 230 | bencher.fb_sockets.push(csock); 231 | 232 | csock.on('error', function (err) { 233 | mod_cmdutil.fail(new VError(err, 'socket error')); 234 | }); 235 | csock.on('connect', function () { 236 | var fastclient = new mod_fast.FastClient({ 237 | 'collector': collector, 238 | 'log': log, 239 | 'transport': csock, 240 | 'nRecentRequests': 5 241 | }); 242 | 243 | bencher.fb_fastclients.push(fastclient); 244 | fastclient.on('error', function (err) { 245 | mod_cmdutil.fail( 246 | new VError(err, 'client error')); 247 | }); 248 | 249 | queuecallback(); 250 | }); 251 | } 252 | }); 253 | 254 | 255 | bencher = new FastBench(); 256 | bencher.fb_config = config; 257 | bencher.fb_sockets = []; 258 | bencher.fb_fastclients = []; 259 | 260 | function _startKang(_, cb) { 261 | mod_kang.knStartServer({ 262 | 'port': config.fbKangPort, 263 | 'uri_base': '/kang', 264 | 'service_name': 'fastbench', 265 | 'version': '1.0.0', 266 | 'ident': mod_os.hostname() + '/' + process.pid, 267 | 'list_types': bencher.kangListTypes.bind(bencher), 268 | 'list_objects': bencher.kangListObjects.bind(bencher), 269 | 'get': bencher.kangGetObject.bind(bencher), 270 | 'stats': bencher.kangStats.bind(bencher) 271 | }, function (err, server) { 272 | mod_assertplus.ok(!err); 273 | bencher.fb_kangserver = server; 274 | log.info({ 'kangPort': config.fbKangPort }, 275 | 'listening for kang requests'); 276 | cb(); 277 | }); 278 | } 279 | 280 | function _startArtedi(_, cb) { 281 | var server; 282 | 283 | function _artediHandler(req, res) { 284 | collector.collect(mod_artedi.FMT_PROM, 285 | function _outputMetrics(err, metrics) { 286 | mod_assertplus.ok(!err); 287 | res.end(metrics); 288 | }); 289 | } 290 | 291 | server = mod_http.createServer(_artediHandler); 292 | server.listen(config.fbArtediPort, function _onListen(err) { 293 | mod_assertplus.ok(!err); 294 | bencher.fb_artediserver = server; 295 | log.info({ artediPort: config.fbArtediPort }, 296 | 'listening for artedi requests'); 297 | cb(); 298 | }); 299 | } 300 | 301 | nconns = Math.ceil(config.fbConcurrency / fbDflRequestsPerConnection); 302 | mod_assertplus.ok(nconns > 0); 303 | for (i = 0; i < nconns; i++) { 304 | queue.push(0); 305 | } 306 | queue.close(); 307 | queue.on('end', function () { 308 | printf('established %d connections to %s:%d\n', 309 | bencher.fb_sockets.length, config.fbHost, config.fbPort); 310 | mod_assertplus.equal(bencher.fb_sockets.length, 311 | bencher.fb_fastclients.length); 312 | 313 | mod_vasync.pipeline({ 314 | funcs: [ 315 | _startKang, 316 | _startArtedi 317 | ] 318 | }, function _pipelineComplete(err) { 319 | mod_assertplus.ok(!err); 320 | 321 | if (config.fbDuration !== null) { 322 | setTimeout(function onCommandTimeout() { 323 | bencher.finish(); 324 | }, config.fbDuration); 325 | } 326 | 327 | process.once('SIGINT', function onSigInt() { 328 | bencher.finish(); 329 | }); 330 | 331 | bencher.start(); 332 | }); 333 | }); 334 | } 335 | 336 | function FastBench() 337 | { 338 | this.fb_sockets = null; /* list of client sockets */ 339 | this.fb_fastclients = null; /* list of fast clients */ 340 | this.fb_config = null; /* configuration */ 341 | 342 | this.fb_queue = null; /* vasync queue for sending requests */ 343 | this.fb_start = null; /* start hrtime */ 344 | this.fb_done = null; /* finish hrtime */ 345 | this.fb_nissued = 0; /* issued RPCs */ 346 | this.fb_ncompleted = 0; /* completed RPCs */ 347 | this.fb_nerrors = 0; /* unexpected errors */ 348 | this.fb_hiwat = 0; /* maximum observed concurrency */ 349 | this.fb_timeout = null; /* reporting timeout */ 350 | this.fb_artediserver = null; /* artedi server */ 351 | this.fb_kangserver = null; /* kang server */ 352 | 353 | /* last reported stats */ 354 | this.fb_reported_ncompleted = 0; 355 | this.fb_reported_nerrors = 0; 356 | } 357 | 358 | FastBench.prototype.start = function () 359 | { 360 | var ntoissue, i; 361 | var self = this; 362 | 363 | mod_assertplus.object(this.fb_sockets); 364 | mod_assertplus.object(this.fb_fastclients); 365 | mod_assertplus.object(this.fb_config); 366 | mod_assertplus.ok(this.fb_queue === null); 367 | mod_assertplus.ok(this.fb_sockets.length > 0); 368 | 369 | this.fb_start = process.hrtime(); 370 | this.fb_queue = mod_vasync.queuev({ 371 | 'concurrency': this.fb_config.fbConcurrency, 372 | 'worker': this.doWork.bind(this) 373 | }); 374 | 375 | /* 376 | * This is kind of a cheesy way to do this. 377 | */ 378 | if (this.fb_config.fbNRequests !== null) { 379 | ntoissue = Math.min(this.fb_config.fbConcurrency, 380 | this.fb_config.fbNRequests); 381 | } else { 382 | ntoissue = this.fb_config.fbConcurrency; 383 | } 384 | 385 | for (i = 0; i < ntoissue; i++) { 386 | this.fb_queue.push(0); 387 | } 388 | 389 | if (this.fb_config.fbReportingInterval > 0) { 390 | this.fb_timeout = setTimeout(function tickForReport() { 391 | self.fb_timeout = setTimeout(tickForReport, 392 | self.fb_config.fbReportingInterval); 393 | self.report(); 394 | }, self.fb_config.fbReportingInterval); 395 | } 396 | }; 397 | 398 | FastBench.prototype.doWork = function (_, queuecb) 399 | { 400 | var self = this; 401 | var concur, client; 402 | 403 | if (this.fb_done !== null) { 404 | setImmediate(queuecb); 405 | return; 406 | } 407 | 408 | this.fb_nissued++; 409 | concur = this.fb_nissued - this.fb_ncompleted; 410 | if (concur > this.fb_hiwat) { 411 | this.fb_hiwat = concur; 412 | } 413 | 414 | client = this.fb_fastclients[ 415 | this.fb_nissued % this.fb_fastclients.length]; 416 | mod_assertplus.object(client); 417 | this.fb_config.fbWorkload.nextRequest(client, function (err) { 418 | if (self.fb_done !== null) { 419 | queuecb(); 420 | return; 421 | } 422 | 423 | if (err) { 424 | self.fb_nerrors++; 425 | } 426 | 427 | self.fb_ncompleted++; 428 | if (self.fb_config.fbNRequests !== null) { 429 | if (self.fb_nissued < self.fb_config.fbNRequests) { 430 | self.fb_queue.push(0); 431 | } else if (self.fb_ncompleted == 432 | self.fb_config.fbNRequests) { 433 | self.finish(); 434 | } 435 | } else { 436 | self.fb_queue.push(0); 437 | } 438 | 439 | queuecb(); 440 | }); 441 | }; 442 | 443 | FastBench.prototype.finish = function () 444 | { 445 | this.fb_done = process.hrtime(); 446 | this.fb_sockets.forEach(function (s) { 447 | s.destroy(); 448 | }); 449 | this.fb_fastclients.forEach(function (fastclient) { 450 | fastclient.detach(); 451 | }); 452 | 453 | if (this.fb_timeout !== null) { 454 | clearTimeout(this.fb_timeout); 455 | } 456 | 457 | this.fb_artediserver.close(); 458 | this.fb_kangserver.close(); 459 | this.printResults(); 460 | }; 461 | 462 | FastBench.prototype.report = function () 463 | { 464 | var elapsed, elapsedms; 465 | var ndone, nerrors; 466 | 467 | elapsed = process.hrtime(this.fb_start); 468 | elapsedms = mod_jsprim.hrtimeMillisec(elapsed); 469 | ndone = this.fb_ncompleted - this.fb_reported_ncompleted; 470 | nerrors = this.fb_nerrors - this.fb_reported_nerrors; 471 | this.fb_reported_ncompleted = this.fb_ncompleted; 472 | this.fb_reported_nerrors = this.fb_nerrors; 473 | 474 | printf('%d %4d.%03dms %d completed, %d failed\n', 475 | Date.now(), Math.floor(elapsedms / 1000), elapsedms % 1000, 476 | ndone, nerrors); 477 | }; 478 | 479 | FastBench.prototype.printResults = function () 480 | { 481 | var duration, durationus; 482 | 483 | duration = mod_jsprim.hrtimeDiff(this.fb_done, this.fb_start); 484 | durationus = mod_jsprim.hrtimeMicrosec(duration); 485 | 486 | printf('-----------------------------------\n'); 487 | printf('total runtime: %d.%06ds\n', 488 | Math.floor(durationus / 1e6), durationus % 1e6); 489 | printf('total requests completed: %d (%d issued)\n', 490 | this.fb_ncompleted, this.fb_nissued); 491 | printf('total unexpected errors: %d\n', this.fb_nerrors); 492 | printf('error rate: %s%%\n', 493 | (100 * this.fb_nerrors / this.fb_ncompleted).toFixed(2)); 494 | printf('maximum concurrency: %d\n', this.fb_hiwat); 495 | printf('request throughput: %d requests per second\n', 496 | this.fb_ncompleted / durationus * 1e6); 497 | 498 | /* 499 | * This really is a rougher estimate than it looks. It's only true if 500 | * we really were at max concurrency the whole time. 501 | */ 502 | printf('estimated average latency: %d us\n', 503 | durationus / (this.fb_ncompleted / this.fb_hiwat)); 504 | }; 505 | 506 | FastBench.prototype.kangStats = function () 507 | { 508 | return ({ 509 | 'nRpcStarted': this.fb_nissued, 510 | 'nRpcDone': this.fb_ncompleted, 511 | 'nErrors': this.fb_nerrors, 512 | 'maxObservedConcurrency': this.fb_hiwat 513 | }); 514 | }; 515 | 516 | FastBench.prototype.kangListTypes = function () 517 | { 518 | if (this.fb_start === null) { 519 | return ([]); 520 | } 521 | 522 | return (this.fb_fastclients[0].kangListTypes()); 523 | }; 524 | 525 | FastBench.prototype.kangListObjects = function (type) 526 | { 527 | /* 528 | * It would be much better if Kang had a better way to aggregate up 529 | * hierarchies of objects. This is pretty cheesy. 530 | */ 531 | var rv = []; 532 | 533 | this.fb_fastclients.forEach(function (fastclient, i) { 534 | var objs = fastclient.kangListObjects(type); 535 | objs.forEach(function (key) { rv.push(i + '/' + key); }); 536 | }); 537 | 538 | return (rv); 539 | }; 540 | 541 | FastBench.prototype.kangGetObject = function (type, id) 542 | { 543 | var parts, client; 544 | 545 | parts = mod_strsplit(id, '/', 2); 546 | mod_assertplus.equal(parts.length, 2); 547 | client = this.fb_fastclients[parts[0]]; 548 | return (client.kangGetObject(type, parts[1])); 549 | }; 550 | 551 | main(); 552 | -------------------------------------------------------------------------------- /bin/fastcall: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* vim: set ft=javascript: */ 3 | /* 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | */ 8 | 9 | /* 10 | * Copyright 2020 Joyent, Inc. 11 | */ 12 | 13 | /* 14 | * fastcall: command-line tool for making a node-fast RPC method call. 15 | */ 16 | 17 | var VError = require('verror'); 18 | 19 | var mod_assertplus = require('assert-plus'); 20 | var mod_bunyan = require('bunyan'); 21 | var mod_cmdutil = require('cmdutil'); 22 | var mod_fast = require('../lib/fast'); 23 | var mod_getopt = require('posix-getopt'); 24 | var mod_net = require('net'); 25 | 26 | var OPTS = { 27 | 'a': 'abandon the RPC request after issuing it ' + 28 | 'via FastClientRequest#abandon', 29 | 'c': 'do not close the socket used to talk ' + 30 | 'to the FastServer until SIGINT' 31 | }; 32 | 33 | function main() 34 | { 35 | var argv, host, port, rpcmethod, rpcargs; 36 | var doabandon = false; 37 | var leaveconnopen = false; 38 | var timeout = null; 39 | 40 | mod_cmdutil.configure({ 41 | 'synopses': [ '[OPTIONS] HOST PORT METHOD ARGS' ], 42 | 'usageMessage': [ 43 | ' OPTIONS', 44 | ' -a,--abandon-immediately ' + OPTS['a'], 45 | ' -c,--leave-conn-open ' + OPTS['c'], 46 | ' HOST DNS name or IP address for remote server', 47 | ' PORT TCP port for remote server', 48 | ' METHOD Name of remote RPC method call', 49 | ' ARGS JSON-encoded arguments for RPC method call' 50 | 51 | ].join('\n') 52 | }); 53 | mod_cmdutil.exitOnEpipe(); 54 | 55 | var option; 56 | var parser = new mod_getopt.BasicParser('a(abandon-immediately)' + 57 | 'c(leave-conn-open)', process.argv); 58 | while ((option = parser.getopt()) !== undefined) { 59 | switch (option.option) { 60 | case 'c': 61 | leaveconnopen = true; 62 | break; 63 | case 'a': 64 | doabandon = true; 65 | break; 66 | default: 67 | mod_assertplus.equal('?', option.option); 68 | mod_cmdutil.usage(); 69 | break; 70 | } 71 | } 72 | 73 | argv = process.argv.slice(parser.optind()); 74 | if (argv.length != 4) { 75 | mod_cmdutil.usage('expected four non-option arguments'); 76 | } 77 | 78 | host = argv[0]; 79 | port = parseInt(argv[1], 10); 80 | if (isNaN(port) || port < 1 || port > 65535) { 81 | mod_cmdutil.usage('invalid TCP port: %s\n', argv[1]); 82 | } 83 | 84 | rpcmethod = argv[2]; 85 | try { 86 | rpcargs = JSON.parse(argv[3]); 87 | } catch (ex) { 88 | mod_cmdutil.usage(new VError(ex, 'parsing RPC arguments')); 89 | } 90 | 91 | if (!Array.isArray(rpcargs)) { 92 | mod_cmdutil.usage(new Error('RPC arguments: expected array')); 93 | } 94 | 95 | fastcall({ 96 | 'host': host, 97 | 'port': port, 98 | 'rpcmethod': rpcmethod, 99 | 'rpcargs': rpcargs, 100 | 'timeout': timeout, 101 | 'abandonImmediately': doabandon, 102 | 'leaveConnOpen': leaveconnopen 103 | }, function (err, result) { 104 | if (err) { 105 | mod_cmdutil.warn(err); 106 | } 107 | }); 108 | } 109 | 110 | function fastcall(args, callback) 111 | { 112 | var log, conn; 113 | var rpcmethod, rpcargs, timeout, doabandon, leaveconnopen; 114 | 115 | mod_assertplus.object(args, 'args'); 116 | mod_assertplus.string(args.host, 'args.host'); 117 | mod_assertplus.number(args.port, 'args.port'); 118 | mod_assertplus.optionalNumber(args.timeout, 'args.timeout'); 119 | mod_assertplus.string(args.rpcmethod, 'args.rpcmethod'); 120 | mod_assertplus.array(args.rpcargs, 'args.rpcargs'); 121 | mod_assertplus.bool(args.abandonImmediately, 'args.abandonImmediately'); 122 | mod_assertplus.bool(args.leaveConnOpen, 'args.leaveConnOpen'); 123 | 124 | rpcmethod = args.rpcmethod; 125 | rpcargs = args.rpcargs; 126 | timeout = args.timeout; 127 | doabandon = args.abandonImmediately; 128 | leaveconnopen = args.leaveConnOpen; 129 | 130 | log = new mod_bunyan({ 131 | 'name': 'fastcall', 132 | 'level': process.env['LOG_LEVEL'] || 'fatal' 133 | }); 134 | 135 | log.info(args, 'fastcall start'); 136 | conn = mod_net.createConnection(args.port, args.host); 137 | 138 | conn.on('connect', function onConnect() { 139 | /* 140 | * If the '--leave-conn-open' option was specified, then we 141 | * leave the connection open until the user sends a SIGINT to 142 | * the process. The purpose of this option is to demonstrate the 143 | * function of FastServer#onConnsDestroyed. See documentation 144 | * on the '--quiesce' option to fastserve for more information. 145 | */ 146 | if (leaveconnopen) { 147 | process.on('SIGINT', function () { 148 | conn.destroy(); 149 | }); 150 | } 151 | 152 | var fastconn, req; 153 | 154 | fastconn = new mod_fast.FastClient({ 155 | 'log': log, 156 | 'transport': conn, 157 | 'nRecentRequests': 10 158 | }); 159 | 160 | fastconn.on('error', function (err) { 161 | if (!leaveconnopen) { 162 | conn.destroy(); 163 | } 164 | callback(new VError(err, 'fast connection')); 165 | }); 166 | 167 | req = fastconn.rpc({ 168 | 'rpcmethod': rpcmethod, 169 | 'rpcargs': rpcargs, 170 | 'timeout': timeout 171 | }); 172 | 173 | if (doabandon) { 174 | req.abandon(); 175 | } 176 | 177 | req.on('error', function (err) { 178 | if (!leaveconnopen) { 179 | conn.destroy(); 180 | } 181 | callback(new VError(err, 'fast request')); 182 | }); 183 | 184 | req.on('data', function (message) { 185 | console.log(JSON.stringify(message)); 186 | }); 187 | 188 | req.on('end', function () { 189 | if (!leaveconnopen) { 190 | conn.destroy(); 191 | } 192 | callback(); 193 | }); 194 | }); 195 | } 196 | 197 | main(); 198 | -------------------------------------------------------------------------------- /bin/fastclatency: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -s 2 | /* 3 | * This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | */ 7 | 8 | /* 9 | * Copyright (c) 2016, Joyent, Inc. 10 | */ 11 | 12 | /* 13 | * fastclatency: dump fast client request latency information 14 | */ 15 | 16 | #pragma D option aggsortkey 17 | #pragma D option aggzoom 18 | #pragma D option quiet 19 | #pragma D option zdefs 20 | 21 | BEGIN 22 | { 23 | printf("Latencies are reported in microseconds. "); 24 | printf("CTRL-C to stop and print results.\n"); 25 | } 26 | 27 | fastclient*:::rpc-start 28 | { 29 | pending[pid, arg0, arg1] = copyinstr(arg2); 30 | starts[pid, arg0, arg1] = timestamp; 31 | } 32 | 33 | fastclient*:::rpc-done 34 | /starts[pid, arg0, arg1] != 0/ 35 | { 36 | @[pending[pid, arg0, arg1]] = 37 | quantize((timestamp - starts[pid, arg0, arg1]) / 1000); 38 | pending[pid, arg0, arg1] = 0; 39 | starts[pid, arg0, arg1] = 0; 40 | } 41 | -------------------------------------------------------------------------------- /bin/fastclatencytime: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -s 2 | /* 3 | * This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | */ 7 | 8 | /* 9 | * Copyright (c) 2016, Joyent, Inc. 10 | */ 11 | 12 | /* 13 | * fastclatencytime: dump fast client request latency information over time 14 | */ 15 | 16 | #pragma D option aggpack 17 | #pragma D option aggsortkey 18 | #pragma D option aggzoom 19 | #pragma D option quiet 20 | #pragma D option zdefs 21 | 22 | BEGIN 23 | { 24 | printf("Latencies are reported in milliseconds. CTRL-C to stop.\n"); 25 | } 26 | 27 | fastclient*:::rpc-start 28 | { 29 | pending[pid, arg0, arg1] = copyinstr(arg2); 30 | starts[pid, arg0, arg1] = timestamp; 31 | } 32 | 33 | fastclient*:::rpc-done 34 | /pending[pid, arg0, arg1] != 0/ 35 | { 36 | @[pending[pid, arg0, arg1], walltimestamp / 1000000000] = 37 | quantize((timestamp - starts[pid, arg0, arg1]) / 1000000); 38 | pending[pid, arg0, arg1] = 0; 39 | starts[pid, arg0, arg1] = 0; 40 | } 41 | -------------------------------------------------------------------------------- /bin/fastcsnoop: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -s 2 | /* 3 | * This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | */ 7 | 8 | /* 9 | * Copyright (c) 2016, Joyent, Inc. 10 | */ 11 | 12 | /* 13 | * fastcsnoop: dump fast client activity 14 | */ 15 | 16 | #pragma D option quiet 17 | #pragma D option zdefs 18 | 19 | BEGIN 20 | { 21 | start = timestamp; 22 | printf("%-8s %6s %3s %9s\n", "TIME", "PID", "CLI", "MSGID"); 23 | } 24 | 25 | fastclient*:::rpc-start, 26 | fastclient*:::rpc-data, 27 | fastclient*:::rpc-done 28 | { 29 | this->d = timestamp - start; 30 | printf("%4d.%03d %6d %3d %9d ", 31 | this->d / 1000000000, 32 | (this->d % 1000000000) / 1000000, 33 | pid, arg0, arg1); 34 | } 35 | 36 | fastclient*:::rpc-start 37 | { 38 | printf("-> %s\n", copyinstr(arg2)); 39 | printf("%37s %s\n", "", copyinstr(arg3)); 40 | pending[pid, arg0, arg1] = copyinstr(arg2); 41 | } 42 | 43 | fastclient*:::rpc-data 44 | { 45 | printf(" data: %s\n", copyinstr(arg2)); 46 | } 47 | 48 | fastclient*:::rpc-done 49 | /pending[pid, arg0, arg1] != 0/ 50 | { 51 | printf("<- %s (status: %s)\n", pending[pid, arg0, arg1], 52 | copyinstr(arg2)); 53 | pending[pid, arg0, arg1] = 0; 54 | } 55 | -------------------------------------------------------------------------------- /bin/fastserve: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* vim: set ft=javascript: */ 3 | /* 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | */ 8 | 9 | /* 10 | * Copyright 2020 Joyent, Inc. 11 | */ 12 | 13 | /* 14 | * fastserve: command-line fast server for demo and testing 15 | */ 16 | 17 | var mod_assertplus = require('assert-plus'); 18 | var mod_artedi = require('artedi'); 19 | var mod_bunyan = require('bunyan'); 20 | var mod_cmdutil = require('cmdutil'); 21 | var mod_getopt = require('posix-getopt'); 22 | var mod_fastdemo = require('../lib/demo_server'); 23 | var mod_fast = require('../lib/fast'); 24 | var mod_jsprim = require('jsprim'); 25 | var mod_kang = require('kang'); 26 | var mod_net = require('net'); 27 | var mod_os = require('os'); 28 | var mod_restify = require('restify'); 29 | var mod_util = require('util'); 30 | 31 | var OPTS = { 32 | '-p,--port': 'fast RPC listening port(default: 2030)', 33 | '-m,--mon-port': 'monitoring server listening port (default: port+800)', 34 | '-q,--quiesce': 'enable quiesce (default: false)' 35 | }; 36 | 37 | /* construct the usage message */ 38 | var usageMessage = ' OPTIONS'; 39 | Object.keys(OPTS).forEach(function (arg) { 40 | usageMessage += mod_util.format('\n\t%s\t%s', arg, OPTS[arg]); 41 | }); 42 | 43 | function main() 44 | { 45 | var option; 46 | 47 | /* default configuration values - monitorPort is set later */ 48 | var config = { 49 | 'quiesce': false, 50 | 'fastPort': 2030 51 | }; 52 | 53 | mod_cmdutil.configure({ 54 | 'synopses': [ '[OPTIONS]' ], 55 | 'usageMessage': usageMessage 56 | }); 57 | mod_cmdutil.exitOnEpipe(); 58 | 59 | var parser = new mod_getopt.BasicParser('p:(port)m:(mon-port)' + 60 | 'q(quiesce)', process.argv); 61 | while ((option = parser.getopt()) !== undefined) { 62 | switch (option.option) { 63 | case 'q': 64 | config.quiesce = true; 65 | break; 66 | case 'p': 67 | config.fastPort = 68 | mod_jsprim.parseInteger(option.optarg); 69 | break; 70 | case 'm': 71 | config.monitorPort = 72 | mod_jsprim.parseInteger(option.optarg); 73 | break; 74 | default: 75 | mod_assertplus.equal('?', option.option); 76 | mod_cmdutil.usage(); 77 | break; 78 | } 79 | } 80 | /* set default monitoring port if none provided */ 81 | if (!config.monitorPort) { 82 | config.monitorPort = config.fastPort + 800; 83 | } 84 | 85 | function isInvalidPort(port) { 86 | return (isNaN(port) || port < 1 || port > 65535); 87 | } 88 | 89 | /* validate port configuration */ 90 | if (isInvalidPort(config.fastPort)) { 91 | mod_cmdutil.usage('invalid fast TCP port: %s\n', 92 | config.fastPort); 93 | } 94 | if (isInvalidPort(config.monitorPort)) { 95 | mod_cmdutil.usage('invalid monitoring TCP port: %s\n', 96 | config.monitorPort); 97 | } 98 | 99 | if (config.fastPort === config.monitorPort) { 100 | mod_cmdutil.usage('fast and monitoring TCP ports must differ:' + 101 | ' %s\n', config.fastPort); 102 | } 103 | 104 | if (parser.optind() !== process.argv.length) { 105 | mod_cmdutil.usage( 106 | 'Positional arguments found when none were expected: %s', 107 | process.argv.slice(parser.optind()).join(' ')); 108 | } 109 | 110 | fastDemoServer(config); 111 | } 112 | 113 | function fastDemoServer(args) 114 | { 115 | var fastPort, log, sock, collector, fastserver; 116 | var monitorPort; 117 | 118 | mod_assertplus.object(args, 'args'); 119 | mod_assertplus.number(args.fastPort, 'args.fastPort'); 120 | mod_assertplus.number(args.monitorPort, 'args.monitorPort'); 121 | mod_assertplus.bool(args.quiesce, 'args.quiesce'); 122 | 123 | collector = mod_artedi.createCollector({ 124 | 'labels': { 125 | 'component': 'fastserve' 126 | } 127 | }); 128 | 129 | log = new mod_bunyan({ 130 | 'name': 'fastserve', 131 | 'level': process.env['LOG_LEVEL'] || 'trace' 132 | }); 133 | 134 | log.info('starting fast server'); 135 | sock = mod_net.createServer({ 'allowHalfOpen': true }); 136 | fastserver = new mod_fast.FastServer({ 137 | 'log': log, 138 | 'collector': collector, 139 | 'server': sock 140 | }); 141 | 142 | mod_fastdemo.demoRpcs().forEach(function (r) { 143 | fastserver.registerRpcMethod(r); 144 | }); 145 | 146 | fastPort = args.fastPort; 147 | monitorPort = args.monitorPort; 148 | sock.listen(fastPort, function () { 149 | var nsigs = 0; 150 | 151 | log.info({ 'fastPort': fastPort }, 152 | 'listening for fast requests'); 153 | 154 | var kangOpts = { 155 | 'uri_base': '/kang', 156 | 'service_name': 'fastserve', 157 | 'version': '1.0.0', 158 | 'ident': mod_os.hostname() + '/' + process.pid, 159 | 'list_types': fastserver.kangListTypes.bind(fastserver), 160 | 'list_objects': fastserver.kangListObjects.bind(fastserver), 161 | 'get': fastserver.kangGetObject.bind(fastserver), 162 | 'stats': fastserver.kangStats.bind(fastserver) 163 | }; 164 | 165 | var monitor_server = mod_restify.createServer({ 166 | name: 'monitor' 167 | }); 168 | monitor_server.get('/metrics', function (req, res, next) { 169 | req.on('end', function () { 170 | collector.collect(mod_artedi.FMT_PROM, 171 | function (err, metrics) { 172 | if (err) { 173 | next(err); 174 | return; 175 | } 176 | res.setHeader('Content-Type', 177 | 'text/plain; version=0.0.4'); 178 | res.send(metrics); 179 | next(); 180 | }); 181 | }); 182 | req.resume(); 183 | }); 184 | 185 | monitor_server.get('/kang/.*', 186 | mod_kang.knRestifyHandler(kangOpts)); 187 | monitor_server.listen(monitorPort, '0.0.0.0', function () { 188 | log.info({ 'monitorPort': monitorPort }, 189 | 'listening for kang and metric requests'); 190 | }); 191 | 192 | process.on('SIGINT', function () { 193 | if (++nsigs == 1) { 194 | sock.close(); 195 | 196 | function shutdown() { 197 | monitor_server.close(); 198 | fastserver.close(); 199 | } 200 | 201 | if (args.quiesce) { 202 | log.info('quiescing server'); 203 | fastserver.onConnsDestroyed(shutdown); 204 | } else { 205 | shutdown(); 206 | } 207 | } 208 | }); 209 | }); 210 | 211 | } 212 | 213 | main(); 214 | -------------------------------------------------------------------------------- /bin/fastslatency: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -s 2 | /* 3 | * This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | */ 7 | 8 | /* 9 | * Copyright (c) 2016, Joyent, Inc. 10 | */ 11 | 12 | /* 13 | * fastslatency: dump fast server request latency information 14 | */ 15 | 16 | #pragma D option aggsortkey 17 | #pragma D option aggzoom 18 | #pragma D option quiet 19 | #pragma D option zdefs 20 | 21 | BEGIN 22 | { 23 | printf("Latencies are reported in microseconds. "); 24 | printf("CTRL-C to stop and print results.\n"); 25 | } 26 | 27 | fastserver*:::rpc-start 28 | { 29 | pending[pid, arg0, arg1, arg2] = copyinstr(arg3); 30 | starts[pid, arg0, arg1, arg2] = timestamp; 31 | } 32 | 33 | fastserver*:::rpc-done 34 | /starts[pid, arg0, arg1, arg2] != 0/ 35 | { 36 | @[pending[pid, arg0, arg1, arg2]] = 37 | quantize((timestamp - starts[pid, arg0, arg1, arg2]) / 1000); 38 | pending[pid, arg0, arg1, arg2] = 0; 39 | starts[pid, arg0, arg1, arg2] = 0; 40 | } 41 | -------------------------------------------------------------------------------- /bin/fastssnoop: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -s 2 | /* 3 | * This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | */ 7 | 8 | /* 9 | * Copyright (c) 2016, Joyent, Inc. 10 | */ 11 | 12 | /* 13 | * fastssnoop: dump fast server activity 14 | */ 15 | 16 | #pragma D option quiet 17 | #pragma D option zdefs 18 | 19 | BEGIN 20 | { 21 | start = timestamp; 22 | printf("%-8s %6s %6s\n", "TIME", "PID", "SRV/CLI"); 23 | } 24 | 25 | fastserver*:::conn-create, 26 | fastserver*:::conn-destroy, 27 | fastserver*:::rpc-start, 28 | fastserver*:::rpc-done 29 | { 30 | this->d = timestamp - start; 31 | printf("%4d.%03d %6d %d/%04d ", 32 | this->d / 1000000000, (this->d % 1000000000) / 1000000, 33 | pid, arg0, arg1); 34 | } 35 | 36 | fastserver*:::conn-create 37 | { 38 | printf("connection created for %s\n", copyinstr(arg2)); 39 | } 40 | 41 | fastserver*:::conn-destroy 42 | { 43 | printf("connection destroyed\n"); 44 | } 45 | 46 | fastserver*:::rpc-start 47 | { 48 | rpcstarts[pid, arg0, arg1, arg2] = timestamp; 49 | rpcmethods[pid, arg0, arg1, arg2] = copyinstr(arg3); 50 | printf("rpc start: %03d (\"%s\")\n", arg2, 51 | rpcmethods[pid, arg0, arg1, arg2]); 52 | } 53 | 54 | fastserver*:::rpc-done 55 | /rpcstarts[pid, arg0, arg1, arg2] == 0/ 56 | { 57 | printf("rpc done: %03d\n", arg2); 58 | } 59 | 60 | fastserver*:::rpc-done 61 | /rpcstarts[pid, arg0, arg1, arg2] != 0/ 62 | { 63 | printf("rpc done: %03d (\"%s\" call took %d us)\n", 64 | arg2, rpcmethods[pid, arg0, arg1, arg2], 65 | (timestamp - rpcstarts[pid, arg0, arg1, arg2]) / 1000); 66 | rpcstarts[pid, arg0, arg1, arg2] = 0; 67 | rpcmethods[pid, arg0, arg1, arg2] = 0; 68 | } 69 | -------------------------------------------------------------------------------- /lib/bench.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | * Copyright (c) 2016, Joyent, Inc. 9 | */ 10 | 11 | /* 12 | * lib/bench.js: common facilities for basic benchmarking 13 | */ 14 | 15 | var mod_assertplus = require('assert-plus'); 16 | var VError = require('verror'); 17 | 18 | exports.fastBenchWorkloads = fastBenchWorkloads; 19 | 20 | var FastBenchWorkloads = { 21 | 'sync': { 22 | 'name': 'sync', 23 | 'description': 'uniform, moderate-sized, synchronous requests', 24 | 'cons': FastWorkloadSync 25 | }, 26 | 27 | 'sleep150': { 28 | 'name': 'sleep150', 29 | 'description': 'uniform, moderate-sized requests with 150ms sleep', 30 | 'cons': FastWorkloadSleep150 31 | } 32 | }; 33 | 34 | function fastBenchWorkloads() 35 | { 36 | return (FastBenchWorkloads); 37 | } 38 | 39 | function FastWorkloadSync() {} 40 | FastWorkloadSync.prototype.name = function () { return ('sync'); }; 41 | FastWorkloadSync.prototype.nextRequest = function (fastclient, callback) 42 | { 43 | return (fastWorkloadRequest({ 44 | 'fastClient': fastclient, 45 | 'delay': null 46 | }, callback)); 47 | }; 48 | 49 | function FastWorkloadSleep150() {} 50 | FastWorkloadSleep150.prototype.name = function () { return ('sleep150'); }; 51 | FastWorkloadSleep150.prototype.nextRequest = function (fastclient, callback) 52 | { 53 | return (fastWorkloadRequest({ 54 | 'fastClient': fastclient, 55 | 'delay': 150 56 | }, callback)); 57 | }; 58 | 59 | function fastWorkloadRequest(args, callback) 60 | { 61 | var fastclient, rpcargs, req, ndata; 62 | 63 | fastclient = args.fastClient; 64 | rpcargs = { 65 | 'rpcmethod': 'fastbench', 66 | 'rpcargs': [ { 67 | 'echo': [ 68 | [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ], 69 | [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ], 70 | [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ], 71 | [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ] 72 | ] 73 | } ] 74 | }; 75 | 76 | if (args.delay !== null) { 77 | rpcargs.rpcargs[0]['delay'] = args.delay; 78 | } 79 | 80 | req = fastclient.rpc(rpcargs); 81 | 82 | ndata = 0; 83 | req.on('data', function (d) { 84 | mod_assertplus.deepEqual(d, 85 | { 'value': [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ] }); 86 | ndata++; 87 | }); 88 | 89 | req.on('end', function (e) { 90 | if (ndata != 4) { 91 | callback(new Error('unexpected data in response')); 92 | } else { 93 | callback(); 94 | } 95 | }); 96 | 97 | req.on('error', function (err) { 98 | callback(new VError(err, 'unexpected server error')); 99 | }); 100 | } 101 | -------------------------------------------------------------------------------- /lib/demo_server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | * Copyright (c) 2016, Joyent, Inc. 9 | */ 10 | 11 | /* 12 | * lib/demo_server.js: implementations of some RPC functions useful in both the 13 | * demo server and test suite. 14 | */ 15 | 16 | var mod_fs = require('fs'); 17 | var mod_lstream = require('lstream'); 18 | var mod_stream = require('stream'); 19 | 20 | var VError = require('verror'); 21 | 22 | exports.demoRpcs = demoRpcs; 23 | 24 | var demoRpc = [ 25 | { 'rpcmethod': 'date', 'rpchandler': fastRpcDate }, 26 | { 'rpcmethod': 'echo', 'rpchandler': fastRpcEcho }, 27 | { 'rpcmethod': 'fail', 'rpchandler': fastRpcFail }, 28 | { 'rpcmethod': 'fastbench', 'rpchandler': fastRpcFastbench }, 29 | { 'rpcmethod': 'sleep', 'rpchandler': fastRpcSleep }, 30 | { 'rpcmethod': 'words', 'rpchandler': fastRpcWords }, 31 | { 'rpcmethod': 'yes', 'rpchandler': fastRpcYes } 32 | ]; 33 | 34 | function demoRpcs() 35 | { 36 | return (demoRpc); 37 | } 38 | 39 | function fastRpcDate(rpc) 40 | { 41 | var when; 42 | 43 | if (rpc.argv().length !== 0) { 44 | rpc.fail(new Error('expected no arguments')); 45 | } else { 46 | when = new Date(); 47 | rpc.end({ 48 | 'timestamp': when.getTime(), 49 | 'iso8601': when.toISOString() 50 | }); 51 | } 52 | } 53 | 54 | function fastRpcEcho(rpc) 55 | { 56 | rpc.argv().forEach(function (a) { rpc.write({ 'value': a }); }); 57 | rpc.end(); 58 | } 59 | 60 | function fastRpcFail(rpc) 61 | { 62 | var errspec, rv; 63 | 64 | if (rpc.argv().length != 1) { 65 | rpc.fail(new Error('expected argument')); 66 | return; 67 | } 68 | 69 | errspec = rpc.argv()[0]; 70 | if (typeof (errspec.name) != 'string' || 71 | typeof (errspec.message) != 'string' || 72 | (errspec.info !== undefined && typeof (errspec.info) != 'object')) { 73 | rpc.fail(new Error('bad arguments')); 74 | return; 75 | } 76 | 77 | if (errspec.data && Array.isArray(errspec.data)) { 78 | errspec.data.forEach(function (d) { 79 | rpc.write({ 'value': d }); 80 | }); 81 | } 82 | 83 | rv = new VError({ 84 | 'name': errspec.name, 85 | 'info': errspec.info || {} 86 | }, '%s', errspec.message); 87 | if (errspec.context) 88 | rv.context = errspec.context; 89 | 90 | setImmediate(function () { rpc.fail(rv); }); 91 | } 92 | 93 | function fastRpcFastbench(rpc) 94 | { 95 | var argv, args; 96 | 97 | argv = rpc.argv(); 98 | if (argv.length != 1 || typeof (argv[0]) != 'object' || 99 | argv[0] === null) { 100 | rpc.fail(new Error('expected exactly one object argument')); 101 | return; 102 | } 103 | 104 | args = argv[0]; 105 | if (!args.hasOwnProperty('echo') || !Array.isArray(args['echo'])) { 106 | rpc.fail(new Error('expected arg.echo')); 107 | return; 108 | } 109 | 110 | if (typeof (args['delay']) == 'number') { 111 | setTimeout(fastRpcFastbenchFinish, args['delay'], rpc, 112 | args['echo']); 113 | } else { 114 | fastRpcFastbenchFinish(rpc, args['echo']); 115 | } 116 | } 117 | 118 | function fastRpcFastbenchFinish(rpc, values) 119 | { 120 | values.forEach(function (a) { rpc.write({ 'value': a }); }); 121 | rpc.end(); 122 | } 123 | 124 | function fastRpcSleep(rpc) 125 | { 126 | var argv, timems, maxtimems; 127 | 128 | argv = rpc.argv(); 129 | if (argv.length != 1) { 130 | rpc.fail(new Error('expected one argument')); 131 | return; 132 | } 133 | 134 | timems = argv[0].ms; 135 | maxtimems = 30 * 60 * 1000; /* 30 minutes */ 136 | if (typeof (timems) != 'number' || timems < 0 || timems > maxtimems) { 137 | rpc.fail(new Error('bad value for "ms"')); 138 | return; 139 | } 140 | 141 | setTimeout(function () { rpc.end(); }, timems); 142 | } 143 | 144 | function fastRpcWords(rpc) 145 | { 146 | var wordfile, wordstream, lstream, xform; 147 | 148 | if (rpc.argv().length !== 0) { 149 | rpc.fail(new Error('expected 0 arguments')); 150 | return; 151 | } 152 | 153 | wordfile = '/usr/dict/words'; 154 | wordstream = mod_fs.createReadStream(wordfile); 155 | lstream = new mod_lstream(); 156 | wordstream.pipe(lstream); 157 | xform = new mod_stream.Transform({ 158 | 'objectMode': true, 159 | 'highWaterMark': 1 160 | }); 161 | xform._transform = function (c, _, callback) { 162 | this.push({ 'word': c }); 163 | setImmediate(callback); 164 | }; 165 | lstream.pipe(xform); 166 | xform.pipe(rpc); 167 | wordstream.on('error', function (err) { 168 | rpc.fail(new VError(err, 'open/read "%s"', wordfile)); 169 | }); 170 | } 171 | 172 | function fastRpcYes(rpc) 173 | { 174 | var argv, value, count, i; 175 | 176 | argv = rpc.argv(); 177 | if (argv.length != 1) { 178 | rpc.fail(new Error('expected one argument')); 179 | return; 180 | } 181 | 182 | value = argv[0].value; 183 | count = argv[0].count; 184 | if (typeof (count) != 'number' || count < 1 || count > 102400) { 185 | rpc.fail(new VError({ 186 | 'info': { 187 | 'foundValue': count, 188 | 'minValue': 1, 189 | 'maxValue': 102400 190 | } 191 | }, 'count must be an integer in range [1, 102400]')); 192 | return; 193 | } 194 | 195 | for (i = 0; i < count; i++) { 196 | rpc.write({ 'value': value }); 197 | } 198 | 199 | rpc.end(); 200 | } 201 | -------------------------------------------------------------------------------- /lib/fast.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | * Copyright 2020 Joyent, Inc. 9 | */ 10 | 11 | /* 12 | * lib/fast.js: public node-fast interface 13 | */ 14 | 15 | var mod_client = require('./fast_client'); 16 | var mod_server = require('./fast_server'); 17 | var mod_protocol = require('./fast_protocol'); 18 | 19 | exports.FastClient = mod_client.FastClient; 20 | exports.FastServer = mod_server.FastServer; 21 | -------------------------------------------------------------------------------- /lib/fast_client_request.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | * Copyright (c) 2018, Joyent, Inc. 9 | */ 10 | 11 | /* 12 | * lib/fast_client_request.js: represents a single RPC request from a fast 13 | * client. This object is the caller's handle on an individual request. 14 | */ 15 | 16 | var mod_assertplus = require('assert-plus'); 17 | var mod_stream = require('stream'); 18 | var mod_util = require('util'); 19 | var VError = require('verror'); 20 | 21 | var mod_protocol = require('./fast_protocol'); 22 | 23 | /* Exported interface */ 24 | exports.FastClientRequest = FastClientRequest; 25 | 26 | /* 27 | * This object is constructed internally by the client interface when beginning 28 | * an RPC request. Arguments include: 29 | * 30 | * client a reference back to the FastClient. Much of the 31 | * functionality of this request is implemented in the 32 | * client. 33 | * 34 | * msgid unique identifier for this request, scoped to this 35 | * transport connection 36 | * 37 | * rpcmethod string name of the remote RPC method to invoke 38 | * 39 | * rpcargs array of arguments to pass to the remote RPC method 40 | * 41 | * ignoreNullValues see "ignoreNullValues" argument to Fast client's rpc() 42 | * method. 43 | * 44 | * log bunyan-style logger 45 | * 46 | * A FastClientRequest object is a client-side caller's handle for an 47 | * outstanding RPC request. From the caller's perspective, fast requests 48 | * normally emit zero or more "data" messages followed by an "end" message. An 49 | * "error" message at any point along the way indicates that the request will 50 | * receive no further messages. 51 | * 52 | * We model this as an object-mode stream. 'data' events are emitted (along 53 | * with the data payload) for each incoming Fast 'data' message. When the 54 | * request completes gracefully, the stream ends (and emits 'end'). If any 55 | * error occurs along the way, 'error' is emitted, and no further 'data' or 56 | * 'end' events will be emitted. Possible sources of error include: 57 | * 58 | * o server-side error: the server explicitly and cleanly sends us an error 59 | * 60 | * o transport or protocol error: we fail the request locally because of an 61 | * error maintaining contact with the server 62 | * 63 | * o local abandonment: the caller abandoned the request 64 | */ 65 | function FastClientRequest(args) 66 | { 67 | mod_assertplus.object(args, 'args'); 68 | mod_assertplus.object(args.client, 'args.client'); 69 | mod_assertplus.number(args.msgid, 'args.msgid'); 70 | mod_assertplus.string(args.rpcmethod, 'args.rpcmethod'); 71 | mod_assertplus.array(args.rpcargs, 'args.rpcargs'); 72 | mod_assertplus.object(args.log, 'args.log'); 73 | mod_assertplus.bool(args.ignoreNullValues, 'args.ignoreNullValues'); 74 | 75 | /* rpc parameters */ 76 | this.frq_client = args.client; 77 | this.frq_msgid = args.msgid; 78 | this.frq_rpcmethod = args.rpcmethod; 79 | this.frq_rpcargs = args.rpcargs; 80 | this.frq_ignorenull = args.ignoreNullValues; 81 | 82 | /* 83 | * RPC state: most RPC requests are immediately transmitted (at least to 84 | * the underlying transport, even if that component ends up queueing 85 | * them). Once that happens, they complete in one of three ways: 86 | * 87 | * o gracefully, when the server sends an "end" or "error" message 88 | * 89 | * o when the local caller issues an abandonment 90 | * 91 | * o when there's an error on the transport, after which we do not 92 | * expect to receive a graceful response 93 | * 94 | * The first two conditions are indicated by frq_done_graceful and 95 | * frq_abandoned. frq_error is set whenever an error is encountered for 96 | * this request, which may be in any of these conditions. 97 | * 98 | * If the transport is disconnected when the user makes the initial 99 | * request, then we never bother to transmit the request. We will set 100 | * frq_skip for this case, though only for debugging purposes. 101 | */ 102 | this.frq_done_graceful = false; /* recvd "end" or "error" from server */ 103 | this.frq_abandoned = false; /* abandoned locally */ 104 | this.frq_error = null; /* error, if any */ 105 | this.frq_hrtstarted = null; /* granular time the request started */ 106 | this.frq_timeout = null; /* timeout handle, if any */ 107 | 108 | /* helpers */ 109 | this.frq_log = args.log; /* logger */ 110 | 111 | /* debugging state */ 112 | this.frq_skip = false; /* RPC was skipped (no transport) */ 113 | this.frq_ndata = 0; /* data messages emitted */ 114 | this.frq_nignored = 0; /* count of ignored messages */ 115 | this.frq_nignored_null = 0; /* count of ignored "null" values */ 116 | this.frq_last = null; /* last message received */ 117 | 118 | /* 119 | * The high watermark is not really used because we do not support flow 120 | * control. 121 | */ 122 | mod_stream.PassThrough.call(this, { 123 | 'objectMode': true, 124 | 'highWaterMark': 16 125 | }); 126 | } 127 | 128 | mod_util.inherits(FastClientRequest, mod_stream.PassThrough); 129 | 130 | FastClientRequest.prototype.abandon = function () 131 | { 132 | /* 133 | * This method is just a convenience alias for the guts that happen in 134 | * the client's requestAbandon() method, where all the real work 135 | * happens. 136 | */ 137 | return (this.frq_client.requestAbandon(this, new VError({ 138 | 'name': 'FastRequestAbandonedError' 139 | }, 'request abandoned by user'))); 140 | }; 141 | 142 | FastClientRequest.prototype.requestId = function () 143 | { 144 | return (this.frq_msgid); 145 | }; 146 | -------------------------------------------------------------------------------- /lib/fast_protocol.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | * Copyright 2020 Joyent, Inc. 9 | */ 10 | 11 | /* 12 | * lib/fast_protocol.js: fast protocol definitions 13 | */ 14 | 15 | var mod_assertplus = require('assert-plus'); 16 | var mod_crc = require('crc'); 17 | var mod_extsprintf = require('extsprintf'); 18 | var mod_old_crc = require('oldcrc'); 19 | var mod_stream = require('stream'); 20 | var mod_util = require('util'); 21 | var VError = require('verror'); 22 | 23 | /* Exported interface */ 24 | exports.fastMessageEncode = fastMessageEncode; 25 | exports.FastMessageEncoder = FastMessageEncoder; 26 | exports.FastMessageDecoder = FastMessageDecoder; 27 | /* Protocol constants are exported below. */ 28 | 29 | /* 30 | * Protocol definition 31 | * 32 | * All fast protocol messages look like the following: 33 | * 34 | * 0x00 +---------+--------+---------+---------+ 35 | * | VERSION | TYPE | STATUS | MSGID1 | 36 | * 0x04 +---------+--------+---------+---------+ 37 | * | MSGID2 | MSGID3 | MSGID4 | CRC1 | 38 | * 0x08 +---------+--------+---------+---------+ 39 | * | CRC2 | CRC3 | CRC4 | DLEN1 | 40 | * 0x0c +---------+--------+---------+---------+ 41 | * | DLEN2 | DLEN3 | DLEN4 | DATA0 | 42 | * 0x10 +---------+--------+---------+---------+ 43 | * | DATAN... | 44 | * +---------+--------+---------+---------+ 45 | * 46 | * VERSION 1-byte integer. The only supported values are "1" and "2". 47 | * 48 | * TYPE 1-byte integer. The only supported value is TYPE_JSON (0x1), 49 | * indicating that the data payload is an encoded JSON object. 50 | * 51 | * STATUS 1-byte integer. The only supported values are: 52 | * 53 | * STATUS_DATA 0x1 indicates a "data" message 54 | * 55 | * STATUS_END 0x2 indicates an "end" message 56 | * 57 | * STATUS_ERROR 0x3 indicates an "error" message 58 | * 59 | * MSGID1...MSGID4 4-byte big-endian unsigned integer, a unique identifier 60 | * for this message 61 | * 62 | * CRC1...CRC4 4-byte big-endian unsigned integer representing the CRC16 63 | * value of the data payload 64 | * 65 | * DLEN0...DLEN4 4-byte big-endian unsigned integer representing the number 66 | * of bytes of data payload that follow 67 | * 68 | * DATA0...DATAN Data payload. This is a JSON-encoded object (for TYPE = 69 | * TYPE_JSON). The encoding length in bytes is given by the 70 | * DLEN0...DLEN4 bytes. 71 | * 72 | * Due to historical bugs in node-crc, the CRC implementation used in version 1 73 | * of the protocol is essentially incompatible with any CRC implementation other 74 | * than the one provided by node-crc version 0.x. The changes to address this 75 | * incompatibility (originally recorded on Github as joyent/node-fast#23) were 76 | * done to create a migration path for clients and servers off of the buggy 77 | * version with minimal operational impact. Clients and servers may select a 78 | * mode of operation where only the buggy CRC calculation is used or where only 79 | * a correct version of the calculation is used. Additionally, servers may elect 80 | * to operate in a mode where messages whose CRC is calculated with either the 81 | * buggy or correct implementation are accepted. In this mode the server will 82 | * use the same CRC calculation method in response messages to a client that the 83 | * client used when encoding the message to transmit to the server. This allows 84 | * for clients to be updated in a gradual manner to support the correct CRC 85 | * calculation. The FAST_CHECKSUM_* constants in this file control the mode of 86 | * operation with respect to the CRC calculation of clients and servers. This 87 | * mechanism can be reused in the event of other such library incompatibilities. 88 | * This mechanism is not intended or required for normal version upgrades to the 89 | * CRC library dependency where there is no change in the result produced by one 90 | * of the CRC calculation methods used by node-fast (crc16 is currently the only 91 | * one used). 92 | */ 93 | 94 | /* 95 | * Message IDs: each Fast message has a message id, which is scoped to the Fast 96 | * connection. We allocate these sequentially from a circular 31-bit space. 97 | */ 98 | var FP_MSGID_MAX = Math.pow(2, 31) - 1; 99 | exports.FP_MSGID_MAX = FP_MSGID_MAX; 100 | 101 | /* 102 | * Field offsets 103 | */ 104 | var FP_OFF_VERSION = 0x0; 105 | var FP_OFF_TYPE = FP_OFF_VERSION + 0x1; /* 0x1 */ 106 | var FP_OFF_STATUS = FP_OFF_TYPE + 0x1; /* 0x2 */ 107 | var FP_OFF_MSGID = FP_OFF_STATUS + 0x1; /* 0x3 */ 108 | var FP_OFF_CRC = FP_OFF_MSGID + 0x4; /* 0x7 */ 109 | var FP_OFF_DATALEN = FP_OFF_CRC + 0x4; /* 0xb */ 110 | var FP_OFF_DATA = FP_OFF_DATALEN + 0x4; /* 0xf */ 111 | exports.FP_OFF_VERSION = FP_OFF_VERSION; 112 | exports.FP_OFF_TYPE = FP_OFF_TYPE; 113 | exports.FP_OFF_STATUS = FP_OFF_STATUS; 114 | exports.FP_OFF_MSGID = FP_OFF_MSGID; 115 | exports.FP_OFF_CRC = FP_OFF_CRC; 116 | exports.FP_OFF_DATALEN = FP_OFF_DATALEN; 117 | exports.FP_OFF_DATA = FP_OFF_DATA; 118 | 119 | 120 | /* size (in bytes) of each message header */ 121 | var FP_HEADER_SZ = FP_OFF_DATA; /* 0xf */ 122 | exports.FP_HEADER_SZ = FP_HEADER_SZ; 123 | 124 | /* possible values for the "status" byte */ 125 | var FP_STATUS_DATA = 0x1; 126 | var FP_STATUS_END = 0x2; 127 | var FP_STATUS_ERROR = 0x3; 128 | exports.FP_STATUS_DATA = FP_STATUS_DATA; 129 | exports.FP_STATUS_END = FP_STATUS_END; 130 | exports.FP_STATUS_ERROR = FP_STATUS_ERROR; 131 | 132 | /* possible values for the "type" byte */ 133 | var FP_TYPE_JSON = 0x1; 134 | exports.FP_TYPE_JSON = FP_TYPE_JSON; 135 | 136 | /* possible values for the "version" byte */ 137 | var FP_VERSION_1 = 0x1; 138 | var FP_VERSION_2 = 0x2; 139 | var FP_VERSION_CURRENT = FP_VERSION_2; 140 | exports.FP_VERSION_1 = FP_VERSION_1; 141 | exports.FP_VERSION_2 = FP_VERSION_2; 142 | exports.FP_VERSION_CURRENT = FP_VERSION_CURRENT; 143 | 144 | // These constants are facilitate an upgrade path from buggy node-crc@0.3.0 145 | var FAST_CHECKSUM_V1 = 0x1; 146 | var FAST_CHECKSUM_V1_V2 = 0x2; 147 | var FAST_CHECKSUM_V2 = 0x3; 148 | exports.FAST_CHECKSUM_V1 = FAST_CHECKSUM_V1; 149 | exports.FAST_CHECKSUM_V1_V2 = FAST_CHECKSUM_V1_V2; 150 | exports.FAST_CHECKSUM_V2 = FAST_CHECKSUM_V2; 151 | 152 | /* 153 | * Encode a logical message for sending over the wire. This requires the 154 | * following named properties: 155 | * 156 | * msgid (number) message identifier -- see lib/fast.js 157 | * 158 | * data (object) represents message contents. At this level, this 159 | * can be any plain-old JavaScript object. 160 | * 161 | * status (number) message "status" (one of FP_STATUS_DATA, FP_STATUS_END, 162 | * or FP_STATUS_ERROR). 163 | * 164 | * Failure to match these requirements is a programmer error that may result in 165 | * a synchronously thrown exception that should not be caught. 166 | */ 167 | function fastMessageEncode(msg) 168 | { 169 | var buffer, data_encoded, datalen, crc16; 170 | 171 | mod_assertplus.object(msg, 'msg'); 172 | mod_assertplus.ok(typeof (msg.msgid) == 'number' && 173 | Math.floor(msg.msgid) == msg.msgid && 174 | msg.msgid >= 0 && msg.msgid <= FP_MSGID_MAX, 175 | 'msg.msgid is not an integer between 0 and FP_MSGID_MAX'); 176 | mod_assertplus.object(msg.data, 'msg.data'); 177 | mod_assertplus.number(msg.status, 'msg.status'); 178 | mod_assertplus.number(msg.version, 'msg.version'); 179 | 180 | switch (msg.status) { 181 | case FP_STATUS_DATA: 182 | case FP_STATUS_END: 183 | case FP_STATUS_ERROR: 184 | break; 185 | default: 186 | throw (new VError('unsupported fast message status')); 187 | } 188 | 189 | data_encoded = JSON.stringify(msg.data); 190 | 191 | /* 192 | * Fast version 1 used a buggy version of the node-crc library so we 193 | * special case it here. All subsequent versions default to using a 194 | * corrected version of the node-crc library. 195 | */ 196 | if (msg.version === FP_VERSION_1) { 197 | crc16 = mod_old_crc.crc16(data_encoded); 198 | } else { 199 | crc16 = mod_crc.crc16(data_encoded); 200 | } 201 | 202 | datalen = Buffer.byteLength(data_encoded); 203 | buffer = new Buffer(FP_HEADER_SZ + datalen); 204 | buffer.writeUInt8(msg.version, FP_OFF_VERSION); 205 | buffer.writeUInt8(FP_TYPE_JSON, FP_OFF_TYPE); 206 | buffer.writeUInt8(msg.status, FP_OFF_STATUS); 207 | buffer.writeUInt32BE(msg.msgid, FP_OFF_MSGID); 208 | buffer.writeUInt32BE(crc16, FP_OFF_CRC); 209 | buffer.writeUInt32BE(datalen, FP_OFF_DATALEN); 210 | buffer.write(data_encoded, FP_OFF_DATA, datalen, 'utf8'); 211 | return (buffer); 212 | } 213 | 214 | /* 215 | * Validate the message CRC based on the protocol version. Returns an 216 | * object that contains the decoded CRC mode if the validation is successful 217 | * or an error if the validation fails. 218 | * 219 | * version The protocol version in use. Used to transition off 220 | * buggy node-crc version. See the comments at the top 221 | * of this module for more details. 222 | * headerCrc The CRC provided in the Fast message header. 223 | * data The string representation of the Fast message data. 224 | */ 225 | function validateCrc(version, headerCrc, data) { 226 | var valid = true; 227 | var error; 228 | 229 | switch (version) { 230 | case FP_VERSION_1: 231 | var v1CalculatedCrc = mod_old_crc.crc16(data); 232 | if (v1CalculatedCrc !== headerCrc) { 233 | valid = false; 234 | error = crcValidationError(headerCrc, v1CalculatedCrc, 235 | version); 236 | } 237 | break; 238 | default: 239 | var v2CalculatedCrc = mod_crc.crc16(data); 240 | if (v2CalculatedCrc !== headerCrc) { 241 | valid = false; 242 | error = crcValidationError(headerCrc, v2CalculatedCrc, 243 | version); 244 | } 245 | break; 246 | } 247 | 248 | return ({ valid: valid, error: error}); 249 | } 250 | 251 | /* 252 | * Return a Verror with details about a CRC validation failure. 253 | * 254 | * headerCrc The CRC provided in the Fast message header. 255 | * calculatedCrc The calculated CRC for the received message. 256 | * version The protocol version in use. 257 | */ 258 | function crcValidationError(headerCrc, calculatedCrc, version) { 259 | var errorObj = { 260 | 'name': 'FastProtocolError', 261 | 'info': { 262 | 'fastReason': 'bad_crc', 263 | 'crcExpected': headerCrc, 264 | 'crcCalculated': calculatedCrc, 265 | 'version': version 266 | } 267 | }; 268 | return (new VError(errorObj, 'fast protocol: expected CRC %s, found %s', 269 | headerCrc, calculatedCrc)); 270 | } 271 | 272 | /* 273 | * Decode a fast message from a buffer that's known to contain a single, 274 | * well-formed message. All of the protocol fields are known to be valid (e.g., 275 | * version, type, status, and msgid) at this point, but the data has not been 276 | * read, so the CRC has not been validated. 277 | */ 278 | function fastMessageDecode(header, buffer) 279 | { 280 | var datalen, datastr, json; 281 | 282 | mod_assertplus.number(header.datalen, 'header.datalen'); 283 | datalen = header.datalen; 284 | mod_assertplus.equal(buffer.length, FP_OFF_DATA + datalen); 285 | datastr = buffer.toString('utf8', FP_OFF_DATA); 286 | 287 | var crcValidationResult = validateCrc(header.version, header.crc, 288 | datastr); 289 | 290 | if (crcValidationResult && !crcValidationResult.valid) { 291 | return (crcValidationResult.error); 292 | } 293 | 294 | try { 295 | json = JSON.parse(datastr); 296 | } catch (ex) { 297 | return (new VError({ 298 | 'name': 'FastProtocolError', 299 | 'cause': ex, 300 | 'info': { 301 | 'fastReason': 'invalid_json' 302 | } 303 | }, 'fast protocol: invalid JSON in "data"')); 304 | } 305 | 306 | if (typeof (json) != 'object' || json === null) { 307 | return (new VError({ 308 | 'name': 'FastProtocolError', 309 | 'info': { 310 | 'fastReason': 'bad_data' 311 | } 312 | }, 'fast protocol: message data must be a non-null object')); 313 | } 314 | 315 | if ((header.status == FP_STATUS_DATA || 316 | header.status == FP_STATUS_END) && !Array.isArray(json.d)) { 317 | return (new VError({ 318 | 'name': 'FastProtocolError', 319 | 'info': { 320 | 'fastReason': 'bad_data_d' 321 | } 322 | }, 'fast protocol: data.d for DATA and END messages must be ' + 323 | 'an array')); 324 | } 325 | 326 | if (header.status == FP_STATUS_ERROR && 327 | (typeof (json.d) != 'object' || json.d === null || 328 | typeof (json.d.name) != 'string' || 329 | typeof (json.d.message) != 'string')) { 330 | return (new VError({ 331 | 'name': 'FastProtocolError', 332 | 'info': { 333 | 'fastReason': 'bad_error' 334 | } 335 | }, 'fast protocol: data.d for ERROR messages must have name ' + 336 | 'and message')); 337 | } 338 | 339 | return ({ 340 | 'status': header.status, 341 | 'msgid': header.msgid, 342 | 'data': json, 343 | 'version': header.version 344 | }); 345 | } 346 | 347 | /* 348 | * Transform stream that takes logical messages and emits a buffer representing 349 | * that message (for sending over the wire). 350 | */ 351 | function FastMessageEncoder() 352 | { 353 | mod_stream.Transform.call(this, { 354 | 'highWaterMark': 16, 355 | 'objectMode': true 356 | }); 357 | } 358 | 359 | mod_util.inherits(FastMessageEncoder, mod_stream.Transform); 360 | 361 | FastMessageEncoder.prototype._transform = function (chunk, _, callback) 362 | { 363 | this.push(fastMessageEncode(chunk)); 364 | setImmediate(callback); 365 | }; 366 | 367 | 368 | /* 369 | * Transform stream that takes bytes (via Buffer objects) and emits an object 370 | * representing the encoded Fast message. 371 | */ 372 | function FastMessageDecoder() 373 | { 374 | mod_stream.Transform.call(this, { 375 | 'objectMode': true 376 | }); 377 | 378 | /* current state */ 379 | this.md_buffer = null; /* unparsed data */ 380 | this.md_havebytes = 0; /* bytes of unparsed data */ 381 | this.md_done = false; /* we've read end-of-stream */ 382 | this.md_error = null; /* fatal error */ 383 | this.md_pushing = false; /* currently calling push() */ 384 | 385 | /* current header */ 386 | this.md_version = null; 387 | this.md_type = null; 388 | this.md_status = null; 389 | this.md_msgid = null; 390 | this.md_crc = null; 391 | this.md_datalen = null; 392 | 393 | /* debug information */ 394 | this.md_nmessages = 0; 395 | this.md_nbytes = 0; 396 | } 397 | 398 | mod_util.inherits(FastMessageDecoder, mod_stream.Transform); 399 | 400 | FastMessageDecoder.prototype._transform = function (chunk, _, callback) 401 | { 402 | this.md_havebytes += chunk.length; 403 | 404 | if (this.md_buffer === null) { 405 | mod_assertplus.equal(this.md_havebytes, chunk.length); 406 | this.md_buffer = chunk; 407 | } else { 408 | this.md_buffer = Buffer.concat( 409 | [ this.md_buffer, chunk ], this.md_havebytes); 410 | } 411 | 412 | this.md_nbytes += chunk.length; 413 | this.decode(callback); 414 | }; 415 | 416 | FastMessageDecoder.prototype._flush = function (callback) 417 | { 418 | this.md_done = true; 419 | this.decode(callback); 420 | }; 421 | 422 | FastMessageDecoder.prototype.decode = function (callback) 423 | { 424 | var buf, msg; 425 | 426 | if (this.md_pushing) { 427 | return; 428 | } 429 | 430 | mod_assertplus.ok(this.md_error === null); 431 | 432 | while (this.md_havebytes >= FP_HEADER_SZ) { 433 | buf = this.md_buffer; 434 | mod_assertplus.equal(buf.length, this.md_havebytes); 435 | mod_assertplus.ok(buf !== null); 436 | mod_assertplus.ok(this.md_error === null); 437 | mod_assertplus.ok(this.md_version === null); 438 | 439 | this.md_version = buf.readUInt8(FP_OFF_VERSION); 440 | if (this.md_version != FP_VERSION_CURRENT && 441 | this.md_version != FP_VERSION_1) { 442 | this.md_error = new VError({ 443 | 'name': 'FastProtocolError', 444 | 'info': { 445 | 'fastReason': 'unsupported_version', 446 | 'foundVersion': this.md_version 447 | } 448 | }, 'fast protocol: unsupported version %d', 449 | this.md_version); 450 | break; 451 | } 452 | 453 | this.md_type = buf.readUInt8(FP_OFF_TYPE); 454 | if (this.md_type != FP_TYPE_JSON) { 455 | this.md_error = new VError({ 456 | 'name': 'FastProtocolError', 457 | 'info': { 458 | 'fastReason': 'unsupported_type', 459 | 'foundType': this.md_type 460 | } 461 | }, 'fast protocol: unsupported type 0x%x', 462 | this.md_type); 463 | break; 464 | } 465 | 466 | this.md_status = buf.readUInt8(FP_OFF_STATUS); 467 | switch (this.md_status) { 468 | case FP_STATUS_DATA: 469 | case FP_STATUS_END: 470 | case FP_STATUS_ERROR: 471 | break; 472 | default: 473 | this.md_error = new VError({ 474 | 'name': 'FastProtocolError', 475 | 'info': { 476 | 'fastReason': 'unsupported_status', 477 | 'foundStatus': this.md_status 478 | } 479 | }, 'fast protocol: unsupported status 0x%x', 480 | this.md_status); 481 | break; 482 | } 483 | 484 | if (this.md_error !== null) { 485 | break; 486 | } 487 | 488 | this.md_msgid = buf.readUInt32BE(FP_OFF_MSGID); 489 | if (this.md_msgid < 0 || this.md_msgid > FP_MSGID_MAX) { 490 | this.md_error = new VError({ 491 | 'name': 'FastProtocolError', 492 | 'info': { 493 | 'fastReason': 'invalid_msgid', 494 | 'foundMsgid': this.md_msgid 495 | } 496 | }, 'fast protocol: invalid msgid %s', this.md_msgid); 497 | break; 498 | } 499 | 500 | this.md_crc = buf.readUInt32BE(FP_OFF_CRC); 501 | this.md_datalen = buf.readUInt32BE(FP_OFF_DATALEN); 502 | 503 | if (this.md_havebytes < FP_HEADER_SZ + this.md_datalen) { 504 | /* 505 | * We don't have enough bytes to continue. Stop now. 506 | * We'll end up re-parsing the header again when we have 507 | * more data. If that turns out to be expensive, we can 508 | * rework this code to keep track of where we were. 509 | */ 510 | this.md_version = null; 511 | this.md_type = null; 512 | this.md_status = null; 513 | this.md_msgid = null; 514 | this.md_crc = null; 515 | this.md_datalen = null; 516 | break; 517 | } 518 | 519 | /* 520 | * We have a complete message. Consume it and update our buffer 521 | * state. 522 | */ 523 | buf = this.md_buffer.slice(0, FP_HEADER_SZ + this.md_datalen); 524 | this.md_buffer = this.md_buffer.slice( 525 | FP_HEADER_SZ + this.md_datalen); 526 | this.md_havebytes -= buf.length; 527 | msg = fastMessageDecode({ 528 | 'version': this.md_version, 529 | 'type': this.md_type, 530 | 'status': this.md_status, 531 | 'msgid': this.md_msgid, 532 | 'crc': this.md_crc, 533 | 'datalen': this.md_datalen 534 | }, buf); 535 | if (msg instanceof Error) { 536 | this.md_error = msg; 537 | break; 538 | } 539 | 540 | this.md_version = null; 541 | this.md_type = null; 542 | this.md_status = null; 543 | this.md_msgid = null; 544 | this.md_crc = null; 545 | this.md_datalen = null; 546 | 547 | this.md_pushing = true; 548 | this.push(msg); 549 | this.md_pushing = false; 550 | this.md_nmessages++; 551 | } 552 | 553 | if (this.md_error === null && this.md_havebytes > 0 && this.md_done) { 554 | this.md_error = new VError({ 555 | 'name': 'FastProtocolError', 556 | 'info': { 557 | 'fastReason': 'incomplete_message' 558 | } 559 | }, 'fast protocol: incomplete message at end-of-stream'); 560 | } 561 | 562 | if (this.md_error !== null) { 563 | setImmediate(callback, this.md_error); 564 | } else { 565 | setImmediate(callback); 566 | } 567 | }; 568 | -------------------------------------------------------------------------------- /lib/subr.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | * Copyright (c) 2016, Joyent, Inc. 9 | */ 10 | 11 | /* 12 | * lib/fast_subr.js: useful utility functions that have not yet been abstracted 13 | * into separate Node modules. 14 | */ 15 | 16 | var mod_assertplus = require('assert-plus'); 17 | 18 | exports.summarizeSocketAddrs = summarizeSocketAddrs; 19 | exports.IdAllocator = IdAllocator; 20 | 21 | /* 22 | * Given a Node socket, return an object summarizing it for debugging purposes. 23 | * It's sad how complicated this is. This is only tested for Node v0.10 and 24 | * v0.12. 25 | */ 26 | function summarizeSocketAddrs(sock) 27 | { 28 | var laddr, rv; 29 | 30 | laddr = sock.address(); 31 | 32 | if (sock.remoteAddress === undefined && 33 | sock.remotePort === undefined && 34 | sock.remoteFamily === undefined) { 35 | return ({ 'socketType': 'UDS (inferred)', 'label': 'UDS' }); 36 | } 37 | 38 | rv = {}; 39 | rv['remoteAddress'] = sock.remoteAddress; 40 | rv['remotePort'] = sock.remotePort; 41 | 42 | if (laddr === null) { 43 | rv['socketType'] = 'unknown'; 44 | rv['label'] = 'unknown'; 45 | } else { 46 | rv['socketType'] = laddr.family ? laddr.family : 'unknown'; 47 | rv['localAddress'] = laddr.address; 48 | rv['localPort'] = laddr.port; 49 | 50 | if (sock.remoteAddress) { 51 | rv['label'] = sock.remoteAddress; 52 | if (sock.remotePort) { 53 | rv['label'] += ':' + sock.remotePort; 54 | } 55 | } else { 56 | rv['label'] = 'unknown'; 57 | } 58 | } 59 | 60 | return (rv); 61 | } 62 | 63 | /* 64 | * IdAllocator is a cheesy interface for allocating non-negative integer 65 | * identifiers. This is similar to the way an OS pid allocator might work, 66 | * where ids are allocated in increasing order to avoid immediate reuse, but ids 67 | * eventually will wrap around. It's expected that callers will use these ids 68 | * as strings (e.g., as object property names). 69 | * 70 | * This is a very simple implementation that we expect to be sufficient for our 71 | * purposes. However, it's not very efficient. We may want to look at 72 | * something like the Bonwick vmem allocator in the future. 73 | * 74 | * This interface considers it a programmer error to attempt to allocate more 75 | * ids than are currently outstanding (i.e., to attempt to allocate when no 76 | * resources are available). That will result in a thrown exception that should 77 | * not be caught. If we want to make this survivable in the future, we could 78 | * improve this, but it's extraordinarily unlikely in the use-cases for which 79 | * this is intended so it's not worth special-casing at this point. 80 | * 81 | * Arguments: 82 | * 83 | * min (number) minimum allowed id (absolute minimum: 0) 84 | * 85 | * max (number) maximum allowed id (absolute maximum: 2^31) 86 | * 87 | * isAllocated function that takes an id and returns whether or not 88 | * (function) the id is still allocated. This is a obviously cheesy, 89 | * but given that callers are keeping track of it, we may as 90 | * well just ask them rather than keep a shadow copy of the 91 | * allocated ids. 92 | * 93 | */ 94 | function IdAllocator(args) 95 | { 96 | mod_assertplus.object(args, 'args'); 97 | mod_assertplus.number(args.min, 'args.min'); 98 | mod_assertplus.number(args.max, 'args.max'); 99 | mod_assertplus.func(args.isAllocated, 'args.isAllocated'); 100 | mod_assertplus.ok(args.min < args.max, 'min must be less than max'); 101 | mod_assertplus.ok(args.min >= 0, 'min must be non-negative'); 102 | mod_assertplus.ok(args.max <= Math.pow(2, 31), 'max is too big'); 103 | 104 | this.ida_min = args.min; 105 | this.ida_max = args.max; 106 | this.ida_isalloc = args.isAllocated; 107 | this.ida_nextid = this.ida_min; 108 | } 109 | 110 | IdAllocator.prototype.alloc = function () 111 | { 112 | var start, next; 113 | 114 | start = this.ida_nextid; 115 | next = start; 116 | while (this.ida_isalloc(next)) { 117 | next++; 118 | 119 | if (next > this.ida_max) { 120 | next = this.ida_min; 121 | } 122 | 123 | if (next == start) { 124 | throw (new Error('all ids allocated')); 125 | } 126 | } 127 | 128 | this.ida_nextid = next + 1; 129 | if (this.ida_nextid > this.ida_max) { 130 | this.ida_nextid = this.ida_min; 131 | } 132 | 133 | return (next); 134 | }; 135 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast", 3 | "description": "streaming JSON RPC over TCP", 4 | "version": "3.1.3", 5 | "main": "./lib/fast.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/joyent/node-fast.git" 9 | }, 10 | "dependencies": { 11 | "assert-plus": "1.0.0", 12 | "bunyan": "^1.7.1", 13 | "cmdutil": "^1.0.0", 14 | "crc": "3.4.4", 15 | "oldcrc": "https://github.com/joyent/node-crc.git#0.3.0-oldcrc", 16 | "extsprintf": "^1.3.0", 17 | "jsprim": "^1.2.2", 18 | "kang": "^1.3.0", 19 | "lstream": "0.0.4", 20 | "microtime": "3.0.0", 21 | "posix-getopt": "1.2.0", 22 | "strsplit": "^1.0.0", 23 | "vasync": "^1.6.3", 24 | "verror": "^1.7.0" 25 | }, 26 | "devDependencies": { 27 | "artedi": "1.1.1", 28 | "forkexec": "^1.0.0", 29 | "restify": "5.2.0" 30 | }, 31 | "optionalDependencies": { 32 | "dtrace-provider": "^0.8.7" 33 | }, 34 | "license": "MIT" 35 | } 36 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | * Copyright 2020 Joyent, Inc. 9 | */ 10 | 11 | /* 12 | * test/common.js: common utilities for test suite 13 | */ 14 | 15 | var mod_assertplus = require('assert-plus'); 16 | var mod_old_crc = require('oldcrc'); 17 | var mod_crc = require('crc'); 18 | var mod_net = require('net'); 19 | var mod_protocol = require('../lib/fast_protocol'); 20 | var mod_stream = require('stream'); 21 | var mod_util = require('util'); 22 | var VError = require('verror'); 23 | 24 | /* IP used for the server in this test suite */ 25 | exports.serverIp = '127.0.0.1'; 26 | /* TCP port used by the server in this test suite */ 27 | exports.serverPort = 31016; 28 | 29 | /* dummy values used as test data */ 30 | exports.dummyRpcMethodName = 'testmethod'; 31 | exports.dummyValue = { 'movies': [ 'red dawn', 'wargames' ] }; 32 | exports.dummyRpcArgs = [ exports.dummyValue ]; 33 | exports.dummyResponseData = { 'd': [ exports.dummyValue ] }; 34 | exports.dummyResponseEndEmpty = { 'd': [] }; 35 | exports.dummyError = new VError({ 36 | 'name': 'DummyError', 37 | 'info': { 38 | 'dummyProp': 'dummyVal' 39 | } 40 | }, 'dummy error message'); 41 | exports.dummyResponseError = { 'd': { 42 | 'name': exports.dummyError.name, 43 | 'message': exports.dummyError.message, 44 | 'info': VError.info(exports.dummyError) 45 | } }; 46 | 47 | exports.makeBigObject = makeBigObject; 48 | exports.writeMessageForEncodedData = writeMessageForEncodedData; 49 | exports.mockServerSetup = mockServerSetup; 50 | exports.mockServerTeardown = mockServerTeardown; 51 | exports.assertRequestError = assertRequestError; 52 | exports.FlowControlSource = FlowControlSource; 53 | exports.isFlowControlled = isFlowControlled; 54 | exports.registerExitBlocker = registerExitBlocker; 55 | exports.unregisterExitBlocker = unregisterExitBlocker; 56 | exports.predatesUsefulPause = predatesUsefulPause; 57 | 58 | /* 59 | * Construct a plain-old-JavaScript object whose size is linear in "width" and 60 | * exponential in "depth". 61 | */ 62 | function makeBigObject(width, depth) 63 | { 64 | var i, rv; 65 | 66 | mod_assertplus.number(width); 67 | mod_assertplus.number(depth); 68 | mod_assertplus.ok(depth >= 1); 69 | 70 | rv = {}; 71 | if (depth === 1) { 72 | for (i = 0; i < width; i++) { 73 | rv['prop_1_' + i] = 'prop_1_' + i + '_value'; 74 | } 75 | } else { 76 | for (i = 0; i < width; i++) { 77 | rv['prop_' + depth + '_' + i] = 78 | makeBigObject(width, depth - 1); 79 | } 80 | } 81 | 82 | return (rv); 83 | } 84 | 85 | /* 86 | * Writes into "buf" (a Node buffer) at offset "msgoffset" a Fast packet with 87 | * message id "msgid", status byte "status", encoded data "dataenc". This 88 | * low-level primitive is provided for the test suite to generate various types 89 | * of invalid messages. If you want to generate valid Fast messages, see the 90 | * MessageEncoder class. 91 | */ 92 | function writeMessageForEncodedData(buf, msgid, status, dataenc, msgoffset, 93 | version) 94 | { 95 | var crc; 96 | if (version === mod_protocol.FP_VERSION_1) { 97 | crc = mod_old_crc.crc16(dataenc); 98 | } else { 99 | crc = mod_crc.crc16(dataenc); 100 | } 101 | var datalen = Buffer.byteLength(dataenc); 102 | 103 | buf.writeUInt8(version || mod_protocol.FP_VERSION_CURRENT, 104 | msgoffset + mod_protocol.FP_OFF_VERSION); 105 | buf.writeUInt8(mod_protocol.FP_TYPE_JSON, 106 | msgoffset + mod_protocol.FP_OFF_TYPE); 107 | buf.writeUInt8(status, msgoffset + mod_protocol.FP_OFF_STATUS); 108 | buf.writeUInt32BE(msgid, msgoffset + mod_protocol.FP_OFF_MSGID); 109 | buf.writeUInt32BE(crc, msgoffset + mod_protocol.FP_OFF_CRC); 110 | buf.writeUInt32BE(datalen, msgoffset + mod_protocol.FP_OFF_DATALEN); 111 | buf.write(dataenc, msgoffset + mod_protocol.FP_OFF_DATA); 112 | } 113 | 114 | /* 115 | * Sets up a server intended for testing. This is little more than a plain TCP 116 | * server, since the mock server needs low-level access to the socket. 117 | * 118 | * Invokes "callback" when the server is ready. 119 | */ 120 | function mockServerSetup(callback) 121 | { 122 | var socket; 123 | 124 | socket = mod_net.createServer({ 'allowHalfOpen': true }); 125 | socket.listen(exports.serverPort, exports.serverIp, function () { 126 | callback(socket); 127 | }); 128 | } 129 | 130 | /* 131 | * Tears down the mock server. 132 | */ 133 | function mockServerTeardown(socket) 134 | { 135 | socket.close(); 136 | } 137 | 138 | /* 139 | * Asserts that the given found_error is a FastRequestError caused by 140 | * expected_cause. 141 | */ 142 | function assertRequestError(found_error, expected_cause) 143 | { 144 | mod_assertplus.equal(found_error.name, 'FastRequestError'); 145 | mod_assertplus.equal(found_error.message, 146 | 'request failed: ' + expected_cause.message); 147 | mod_assertplus.equal(found_error.cause().name, expected_cause.name); 148 | } 149 | 150 | 151 | /* 152 | * A FlowControlSource is an object-mode Readable stream that emits data until 153 | * the caller calls stop(). This class emits event 'resting' when it has been 154 | * flow controlled for the specified time. 155 | * 156 | * datum chunk of data to emit when asked for data 157 | * 158 | * log bunyan-style logger 159 | * 160 | * restMs time to wait while flow-controlled before emitting 161 | * 'resting' 162 | */ 163 | function FlowControlSource(args) 164 | { 165 | mod_assertplus.object(args, 'args'); 166 | mod_assertplus.object(args.datum, 'args.datum'); 167 | mod_assertplus.object(args.log, 'args.log'); 168 | mod_assertplus.number(args.restMs, 'args.restMs'); 169 | 170 | this.fcs_datum = args.datum; 171 | this.fcs_log = args.log; 172 | this.fcs_rest_time = args.restMs; 173 | this.fcs_reading = false; 174 | this.fcs_stopped = false; 175 | this.fcs_flowcontrolled = null; 176 | this.fcs_timeout = null; 177 | this.fcs_ntransients = 0; 178 | this.fcs_nresting = 0; 179 | this.fcs_nwritten = 0; 180 | 181 | mod_stream.Readable.call(this, { 182 | 'objectMode': true, 183 | 'highWaterMark': 16 184 | }); 185 | } 186 | 187 | mod_util.inherits(FlowControlSource, mod_stream.Readable); 188 | 189 | FlowControlSource.prototype._read = function () 190 | { 191 | var i; 192 | 193 | if (this.fcs_reading) { 194 | this.fcs_log.debug('ignoring _read(): already reading'); 195 | return; 196 | } 197 | 198 | if (this.fcs_stopped) { 199 | this.fcs_log.debug('_read() pushing end-of-stream'); 200 | this.push(null); 201 | return; 202 | } 203 | 204 | this.fcs_reading = true; 205 | if (this.fcs_timeout !== null) { 206 | this.fcs_ntransients++; 207 | clearTimeout(this.fcs_timeout); 208 | this.fcs_timeout = null; 209 | } 210 | 211 | this.fcs_log.trace('reading'); 212 | for (i = 1; ; i++) { 213 | this.fcs_nwritten++; 214 | if (!this.push(this.fcs_datum)) { 215 | break; 216 | } 217 | } 218 | 219 | this.fcs_log.trace({ 220 | 'nwritten': this.fcs_nwritten, 221 | 'ntransients': this.fcs_ntransients, 222 | 'nresting': this.fcs_nresting 223 | }, 'flow-controlled after %d objects', i); 224 | this.fcs_flowcontrolled = new Date(); 225 | this.fcs_timeout = setTimeout(this.onTimeout.bind(this), 226 | this.fcs_rest_time); 227 | this.fcs_reading = false; 228 | }; 229 | 230 | FlowControlSource.prototype.stop = function () 231 | { 232 | this.fcs_stopped = true; 233 | this._read(); 234 | }; 235 | 236 | FlowControlSource.prototype.onTimeout = function () 237 | { 238 | var state = { 239 | 'nwritten': this.fcs_nwritten, 240 | 'ntransients': this.fcs_ntransients, 241 | 'nresting': this.fcs_nresting 242 | }; 243 | 244 | this.fcs_nresting++; 245 | this.fcs_timeout = null; 246 | this.fcs_log.debug(state, 'coming to rest'); 247 | this.emit('resting', state); 248 | }; 249 | 250 | 251 | /* 252 | * Returns true only if the given stream appears to be flow-controlled. 253 | * 254 | * These checks are brittle because they depend on internal Node implementation 255 | * details. However, if those details change, the failure here is likely to be 256 | * explicit, and we can decide how best to fix them. We could skip these checks 257 | * entirely, but we'd like to be sure that the flow-control mechanism was 258 | * definitely engaged and that it was engaged because the client is backed up. 259 | * If this check fails, and the Node internals on which it relies have not 260 | * changed, that means that we inadvertently decided the server was 261 | * flow-controlled above even though the client's buffer is not full. 262 | */ 263 | function isFlowControlled(stream) 264 | { 265 | mod_assertplus.equal('number', typeof (stream._readableState.length)); 266 | mod_assertplus.equal('number', 267 | typeof (stream._readableState.highWaterMark)); 268 | return (stream._readableState.length >= 269 | stream._readableState.highWaterMark); 270 | } 271 | 272 | 273 | /* 274 | * Register that the current program should not exit until this registration is 275 | * removed. It's sometimes easy to create Node programs that exit prematurely 276 | * with status 0 because they had no more work to do. This mechanism causes the 277 | * program to crash when that happens. You first register an "exit blocker" 278 | * with a descriptive name (so that you know which part of the program thinks it 279 | * should not be exiting). On normal process exit (i.e. with status code 0), if 280 | * the exit blocker has not been removed, then the program crashes. Exits for 281 | * non-zero status codes (including aborts due to --abort-on-uncaught-exception 282 | * or other fatal errors) are not affected by this. 283 | * 284 | * This would be a useful first-class interface, maybe in node-vasync. 285 | */ 286 | var exitBlockers = {}; 287 | function registerExitBlocker(name) 288 | { 289 | var onExitHandler; 290 | 291 | mod_assertplus.ok(!exitBlockers.hasOwnProperty(name), 292 | 'exit blocker "' + name + '" is already registered'); 293 | onExitHandler = function (code) { 294 | if (code === 0) { 295 | throw (new VError('premature exit: blocker "%s" is ' + 296 | 'still regisered', name)); 297 | } 298 | }; 299 | 300 | exitBlockers[name] = onExitHandler; 301 | process.on('exit', onExitHandler); 302 | } 303 | 304 | function unregisterExitBlocker(name) 305 | { 306 | mod_assertplus.ok(exitBlockers.hasOwnProperty(name), 307 | 'exit blocker "' + name + '" is not registered'); 308 | process.removeListener('exit', exitBlockers[name]); 309 | delete (exitBlockers[name]); 310 | } 311 | 312 | /* 313 | * Returns true if the current Node version predates a pause() function that 314 | * works on all streams. In Node v0.10, if you pause a stream that was 315 | * constructed in the "new" mode, you get an error about "Cannot switch to old 316 | * mode now", which makes it harder for us to exercise certain test cases. 317 | */ 318 | function predatesUsefulPause() 319 | { 320 | return (/^v0\.10\./.test(process.version)); 321 | } 322 | -------------------------------------------------------------------------------- /test/common/client.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | * Copyright 2020 Joyent, Inc. 9 | */ 10 | 11 | /* 12 | * test/common/client.js: common facilities for testing the client 13 | */ 14 | 15 | var mod_assertplus = require('assert-plus'); 16 | var mod_net = require('net'); 17 | 18 | var mod_client = require('../../lib/fast_client'); 19 | var mod_protocol = require('../../lib/fast_protocol'); 20 | var mod_testcommon = require('../common'); 21 | 22 | exports.ClientTestContext = ClientTestContext; 23 | 24 | /* 25 | * The ClientTestContext class provides common functions for setting up a 26 | * FastClient and connecting it to a mock server. This class logs all activity 27 | * and keeps track of events emitted. 28 | * 29 | * Note that this class is just a convenience. It doesn't do any real 30 | * implementation hiding. Callers are free to mess with internal members as 31 | * needed to exercise various functionality. 32 | */ 33 | function ClientTestContext(args) 34 | { 35 | mod_assertplus.object(args.server); 36 | mod_assertplus.object(args.log); 37 | mod_assertplus.optionalObject(args.collector); 38 | mod_assertplus.optionalNumber(args.client_version); 39 | 40 | this.ctc_collector = args.collector; /* artedi collector */ 41 | this.ctc_log = args.log; /* bunyan logger */ 42 | this.ctc_closed = false; /* already cleaned up */ 43 | 44 | this.client_version = args.client_version || 45 | mod_protocol.FP_VERSION_CURRENT; 46 | 47 | /* server handles */ 48 | this.ctc_server = args.server; /* server listening socket */ 49 | this.ctc_server_sock = null; /* server-side connection to client */ 50 | this.ctc_server_message = null; /* first message received by server */ 51 | this.ctc_server_decoder = null; /* decoder piped from ctc_server_sock */ 52 | this.ctc_server_encoder = null; /* encoder piped to ctc_server_sock */ 53 | 54 | /* client handles */ 55 | this.ctc_client_sock = null; /* client TCP socket */ 56 | this.ctc_fastclient = null; /* FastClient handle */ 57 | 58 | /* client events emitted */ 59 | this.ctc_error_client = null; /* emitted on this.ctc_fastclient */ 60 | this.ctc_error_sock = null; /* emitted on this.ctc_client_sock */ 61 | } 62 | 63 | /* 64 | * Creates a Fast client and connects it to the server. 65 | */ 66 | ClientTestContext.prototype.establishConnection = function () 67 | { 68 | var self = this; 69 | 70 | mod_assertplus.ok(!this.ctc_closed); 71 | this.ctc_client_sock = mod_net.createConnection( 72 | mod_testcommon.serverPort, mod_testcommon.serverIp); 73 | this.ctc_fastclient = new mod_client.FastClient({ 74 | 'collector': this.ctc_collector, 75 | 'log': this.ctc_log.child({ 'component': 'FastClient' }), 76 | 'nRecentRequests': 100, 77 | 'transport': this.ctc_client_sock, 78 | 'version': this.client_version 79 | }); 80 | 81 | this.ctc_fastclient.on('error', function (err) { 82 | self.ctc_log.debug(err, 'client error'); 83 | mod_assertplus.ok(self.ctc_error_client === null, 84 | 'client emitted more than one error'); 85 | self.ctc_error_client = err; 86 | }); 87 | 88 | this.ctc_server.once('connection', function (sock) { 89 | mod_assertplus.ok(self.ctc_server_sock === null); 90 | self.ctc_log.debug('server accepted connection'); 91 | self.ctc_server_sock = sock; 92 | self.ctc_server_encoder.pipe(sock); 93 | sock.pipe(self.ctc_server_decoder); 94 | }); 95 | 96 | this.ctc_server_encoder = 97 | new mod_protocol.FastMessageEncoder(); 98 | this.ctc_server_decoder = 99 | new mod_protocol.FastMessageDecoder(); 100 | }; 101 | 102 | /* 103 | * Instructs that the server should handle the next RPC request that it sees. 104 | */ 105 | ClientTestContext.prototype.handleNextRequest = function (options) 106 | { 107 | var self = this; 108 | mod_assertplus.bool(options.data); 109 | mod_assertplus.bool(options.error); 110 | 111 | mod_assertplus.ok(!this.ctc_closed); 112 | this.ctc_server_decoder.once('data', function (message) { 113 | self.ctc_log.debug({ 114 | 'msgid': message.msgid 115 | }, 'server got request'); 116 | 117 | self.ctc_server_message = message; 118 | self.serverReply(message, options); 119 | }); 120 | }; 121 | 122 | /* 123 | * Reply to the given RPC request. 124 | */ 125 | ClientTestContext.prototype.serverReply = function (message, options) 126 | { 127 | mod_assertplus.object(options); 128 | mod_assertplus.bool(options.data); 129 | mod_assertplus.bool(options.error); 130 | 131 | if (options.data) { 132 | this.ctc_server_encoder.write({ 133 | 'msgid': message.msgid, 134 | 'status': mod_protocol.FP_STATUS_DATA, 135 | 'data': mod_testcommon.dummyResponseData, 136 | 'version': this.client_version 137 | }); 138 | } 139 | 140 | if (options.error) { 141 | this.ctc_server_encoder.write({ 142 | 'msgid': message.msgid, 143 | 'status': mod_protocol.FP_STATUS_ERROR, 144 | 'data': mod_testcommon.dummyResponseError, 145 | 'version': this.client_version 146 | }); 147 | } else { 148 | this.ctc_server_encoder.write({ 149 | 'msgid': message.msgid, 150 | 'status': mod_protocol.FP_STATUS_END, 151 | 'data': mod_testcommon.dummyResponseData, 152 | 'version': this.client_version 153 | }); 154 | } 155 | }; 156 | 157 | /* 158 | * Direct the client to execute an RPC request. Returns a ClientTestRequest, 159 | * which keeps track of events emitted on the request. 160 | */ 161 | ClientTestContext.prototype.makeRequest = function (callback) 162 | { 163 | return (this.makeRequestWithOptions({}, callback)); 164 | }; 165 | 166 | ClientTestContext.prototype.makeRequestWithOptions = 167 | function (options, callback) 168 | { 169 | var req, log; 170 | var ctr = new ClientTestRequest(this); 171 | 172 | mod_assertplus.ok(!this.ctc_closed); 173 | ctr.ctr_data = []; 174 | 175 | req = ctr.ctr_request = this.ctc_fastclient.rpc({ 176 | 'ignoreNullValues': options.ignoreNullValues, 177 | 'rpcmethod': mod_testcommon.dummyRpcMethodName, 178 | 'rpcargs': mod_testcommon.dummyRpcArgs 179 | }); 180 | 181 | ctr.ctr_log = this.ctc_log.child({ 'requestId': req.requestId() }); 182 | log = ctr.ctr_log; 183 | log.debug('issued RPC'); 184 | req.on('data', function (d) { 185 | log.debug(d, 'request data'); 186 | ctr.ctr_data.push(d); 187 | }); 188 | 189 | req.on('end', function () { 190 | log.debug('request end'); 191 | mod_assertplus.ok(!ctr.ctr_done); 192 | ctr.ctr_done = true; 193 | /* 194 | * This relies a bit on implicit semantics, but we invoke the 195 | * callback on the next tick so that if the client object emits 196 | * an error after this event, the caller will be able to see 197 | * that. 198 | */ 199 | setImmediate(callback); 200 | }); 201 | 202 | req.on('error', function (err) { 203 | log.debug(err, 'request error'); 204 | mod_assertplus.ok(!ctr.ctr_done); 205 | ctr.ctr_done = true; 206 | ctr.ctr_error = err; 207 | setImmediate(callback); 208 | }); 209 | 210 | return (ctr); 211 | }; 212 | 213 | /* 214 | * Clean up the client and server connections. This does not close the 215 | * listening socket. 216 | */ 217 | ClientTestContext.prototype.cleanup = function () 218 | { 219 | mod_assertplus.ok(!this.ctc_closed); 220 | this.ctc_closed = true; 221 | this.ctc_client_sock.destroy(); 222 | 223 | if (this.ctc_server_sock !== null) { 224 | this.ctc_server_sock.destroy(); 225 | } 226 | }; 227 | 228 | 229 | /* 230 | * This helper class keeps track of the state of a single client request. 231 | */ 232 | function ClientTestRequest(ctc) 233 | { 234 | this.ctr_context = ctc; /* parent ClientTestContext */ 235 | this.ctr_request = null; /* FastClientRequest object */ 236 | this.ctr_error = null; /* "error" emitted */ 237 | this.ctr_data = []; /* "data" events emitted */ 238 | this.ctr_done = false; /* request has completed */ 239 | this.ctr_log = null; /* bunyan logger */ 240 | } 241 | -------------------------------------------------------------------------------- /test/compat/Makefile.compat.defs: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2016, Joyent, Inc. All rights reserved. 3 | # 4 | # Makefile.compat.defs: defines variables used for setting up and running the 5 | # Fast library compatibility tests. 6 | # 7 | 8 | FAST_COMPAT_TESTDIR = test/compat 9 | NODE ?= $(error expected NODE to be defined in Makefile) 10 | -------------------------------------------------------------------------------- /test/compat/Makefile.compat.targ: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2016, Joyent, Inc. All rights reserved. 3 | # 4 | # Makefile.compat.targ: defines targets used for setting up and running the Fast 5 | # library compatibility tests. 6 | # 7 | 8 | # 9 | # We normally despise monolithic targets that do a bunch of work like this. 10 | # It's typically much better to have them depend on intermediate targets that do 11 | # the real work so that the target is interruptible, incremental, and 12 | # parallelizable. In this case, the main output is the local copy of the 13 | # node-fast module, but the presence of that module does not mean it was 14 | # successfully installed. 15 | # 16 | test-compat: 17 | (cd $(FAST_COMPAT_TESTDIR) && $(NODE) setup.js && \ 18 | $(NODE) manual-tst.client_compat.js) 19 | CLEAN_FILES += $(FAST_COMPAT_TESTDIR)/node_modules 20 | -------------------------------------------------------------------------------- /test/compat/common.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | * Copyright (c) 2016, Joyent, Inc. 9 | */ 10 | 11 | /* 12 | * test/compat/common.js: common functions for testing compatiblity of this Fast 13 | * implementation with an older one. This is a pain to test because because 14 | * this module depends on Node v0.12 or later (because of cueball) while 15 | * versions of the Fast server in use today cannot work on Node v0.12 (because 16 | * of breaking changes to the V8 API across major versions). As a result, in 17 | * order to test this, we need to start a Fast server using a separate version 18 | * of Node, with a separate node_modules hierarchy that includes the old Fast 19 | * version. 20 | */ 21 | 22 | var mod_assertplus = require('assert-plus'); 23 | var mod_child = require('child_process'); 24 | var mod_forkexec = require('forkexec'); 25 | var mod_fs = require('fs'); 26 | var mod_net = require('net'); 27 | var mod_path = require('path'); 28 | var mod_vasync = require('vasync'); 29 | var VError = require('verror'); 30 | 31 | exports.nodeConfigLoad = nodeConfigLoad; 32 | exports.setupOldServer = setupOldServer; 33 | exports.teardownOldServer = teardownOldServer; 34 | 35 | /* 36 | * Uses the FAST_COMPAT_NODEDIR environment variable to construct paths to the 37 | * "node" and "npm" executables and then sanity-checks the results. If that 38 | * fails for any reason (including a missing environment variable or the node 39 | * appears to point to the wrong version), emits an error. 40 | */ 41 | function nodeConfigLoad(callback) 42 | { 43 | var nodedir, nodebin, npmbin; 44 | 45 | if (!process.env['FAST_COMPAT_NODEDIR']) { 46 | setImmediate(callback, new VError('The compatibility tests ' + 47 | 'require that the FAST_COMPAT_NODEDIR environment ' + 48 | 'variable refer to the directory of a Node 0.10 ' + 49 | 'installation')); 50 | return; 51 | } 52 | 53 | nodedir = process.env['FAST_COMPAT_NODEDIR']; 54 | nodebin = mod_path.join(nodedir, 'bin', 'node'); 55 | npmbin = mod_path.join(nodedir, 'bin', 'npm'); 56 | 57 | process.stderr.write('checking node version ... '); 58 | mod_forkexec.forkExecWait({ 59 | 'argv': [ nodebin, '-v' ] 60 | }, function (err, info) { 61 | if (err) { 62 | process.stderr.write('FAIL\n'); 63 | callback(err); 64 | return; 65 | } 66 | 67 | process.stderr.write(info.stdout); 68 | if (!/^v0\.10\./.test(info.stdout)) { 69 | callback(new VError('$FAST_COMPAT_NODEDIR/bin/' + 70 | 'node does not appear to be v0.10')); 71 | } else { 72 | callback(null, { 73 | 'nodebin': nodebin, 74 | 'npmbin': npmbin 75 | }); 76 | } 77 | }); 78 | } 79 | 80 | /* 81 | * Instantiates an old-version Fast server and invokes "callback" once the 82 | * server is listening. The callback may be invoked with an error. 83 | * 84 | * ip IP address for the old server to listen on 85 | * 86 | * port TCP port for the old server to listen on 87 | */ 88 | function setupOldServer(args, callback) 89 | { 90 | var nodebin, testdir, serverbin, child; 91 | 92 | mod_assertplus.object(args, 'args'); 93 | mod_assertplus.string(args.ip, 'args.ip'); 94 | mod_assertplus.ok(mod_net.isIP(args.ip), 'args.ip is an IP address'); 95 | mod_assertplus.number(args.port, 'args.port'); 96 | mod_assertplus.func(callback, 'callback'); 97 | 98 | testdir = __dirname; 99 | serverbin = mod_path.join(testdir, 'legacy-server.js'); 100 | 101 | mod_vasync.pipeline({ 'funcs': [ 102 | function loadConfig(_, next) { 103 | nodeConfigLoad(function (err, nc) { 104 | if (!err) { 105 | mod_assertplus.object(nc); 106 | mod_assertplus.string(nc.nodebin); 107 | nodebin = nc.nodebin; 108 | } 109 | 110 | next(err); 111 | }); 112 | }, 113 | 114 | function startOldServer(_, next) { 115 | var onexit, onerror; 116 | 117 | console.error('starting legacy server ... '); 118 | child = mod_child.spawn(nodebin, 119 | [ serverbin, '--test-mode', args.ip, args.port ], 120 | { 121 | 'stdio': [ 122 | process.stdin, 123 | process.stdout, 124 | process.stderr, 125 | 'pipe' 126 | ] 127 | }); 128 | 129 | onexit = function () { 130 | next(new VError('child unexpectedly exited (have ' + 131 | 'you set up this repo for compatibility tests?')); 132 | }; 133 | child.on('exit', onexit); 134 | 135 | onerror = function (err) { 136 | next(new VError(err, 'child spawn failed')); 137 | }; 138 | child.on('error', onerror); 139 | 140 | child.stdio[3].once('data', function () { 141 | /* 142 | * Having received any data at all on this file 143 | * descriptor indicates the server is now listening. 144 | */ 145 | child.removeListener('onerror', onerror); 146 | child.removeListener('exit', onexit); 147 | next(); 148 | }); 149 | } 150 | ] }, function (err) { 151 | if (err) { 152 | callback(err); 153 | } else { 154 | callback(null, child); 155 | } 156 | }); 157 | } 158 | 159 | function teardownOldServer(child, callback) 160 | { 161 | process.stderr.write('tearing down server ... '); 162 | child.on('exit', function () { 163 | process.stderr.write('done.\n'); 164 | callback(); 165 | }); 166 | child.kill('SIGKILL'); 167 | } 168 | -------------------------------------------------------------------------------- /test/compat/legacy-server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | * Copyright (c) 2016, Joyent, Inc. 9 | */ 10 | 11 | /* 12 | * test/compat/legacy-server.js: start an old-version Fast server listening on 13 | * the specified port. 14 | */ 15 | 16 | /* 17 | * NOTE: Unlike the rest of this project, this code is executed with Node 0.10! 18 | * WE should not make use of any dependencies aside from the legacy node-fast 19 | * implementation. If we need to use more dependencies, we need to update 20 | * setupOldServer() to make sure these are installed. 21 | */ 22 | var mod_fs = require('fs'); 23 | var mod_legacyfast = require('fast'); 24 | var mod_net = require('net'); 25 | var mod_path = require('path'); 26 | var server; 27 | 28 | function usage() 29 | { 30 | console.error('usage: node %s IP PORT', mod_path.basename(__filename)); 31 | process.exit(2); 32 | } 33 | 34 | function main() 35 | { 36 | var ip, port, args, testmode; 37 | 38 | args = process.argv.slice(2); 39 | if (args.length > 0 && args[0] == '--test-mode') { 40 | testmode = true; 41 | args.shift(); 42 | } 43 | 44 | if (args.length != 2) { 45 | usage(); 46 | } 47 | 48 | ip = args[0]; 49 | port = args[1]; 50 | if (!mod_net.isIP(ip) || isNaN(port) || port <= 0 || port > 65535) { 51 | console.error('bad IP or port number'); 52 | usage(); 53 | } 54 | 55 | server = mod_legacyfast.createServer(); 56 | console.error('legacy server: startup (pid %d)', process.pid); 57 | server.listen(port, ip, function () { 58 | console.error('legacy server: listening on %s:%d', ip, port); 59 | setupRpcHandlers(); 60 | 61 | if (testmode) { 62 | exitWhenParentDies(); 63 | notifyParent(); 64 | } 65 | }); 66 | } 67 | 68 | /* 69 | * The way this program is launched, our parent process is sitting on the other 70 | * end of the pipe at fd 3, and we notify it that we're listening by writing 71 | * data on this pipe. 72 | */ 73 | function notifyParent() 74 | { 75 | var message, buf; 76 | 77 | message = 'server ready'; 78 | buf = new Buffer(Buffer.byteLength(message, 'utf8')); 79 | buf.write(message); 80 | mod_fs.write(3, buf, 0, buf.length, null, function (err) { 81 | if (err) { 82 | console.error('legacy server: error writing to ' + 83 | 'parent: %s', err.message); 84 | process.exit(1); 85 | } 86 | }); 87 | } 88 | 89 | /* 90 | * This program should exit when the parent goes away. This mechanism only 91 | * works because the parent has set up a pipe on fd 3, which will be closed when 92 | * that process exits. 93 | */ 94 | function exitWhenParentDies() 95 | { 96 | var buf = new Buffer(1); 97 | mod_fs.read(3, buf, 0, 1, null, function (err) { 98 | console.error('legacy server: ' + 99 | 'terminating after read from parent'); 100 | server.close(); 101 | }); 102 | } 103 | 104 | function setupRpcHandlers() 105 | { 106 | server.rpc('echo', function () { 107 | var response, args; 108 | 109 | /* 110 | * The varargs behavior of the original API makes this the 111 | * simplest way to get the arguments. 112 | */ 113 | response = arguments[arguments.length - 1]; 114 | args = Array.prototype.slice.call( 115 | arguments, 0, arguments.length - 1); 116 | if (args.length == 1 && args[0] === null) { 117 | response.end(null); 118 | return; 119 | } 120 | 121 | if (args.length != 1 || typeof (args[0]) != 'object' || 122 | !Array.isArray(args[0].values) || 123 | typeof (args[0].errorResult) != 'boolean') { 124 | response.end(new Error('bad arguments')); 125 | return; 126 | } 127 | 128 | args[0].values.forEach(function (v) { 129 | response.write(v); 130 | }); 131 | 132 | if (args[0].errorResult) { 133 | var err = new Error('boom boom!'); 134 | err.context = { 'result': 'poof' }; 135 | response.end(err); 136 | } else { 137 | response.end(); 138 | } 139 | }); 140 | 141 | server.rpc('fastbench', function () { 142 | var response, args; 143 | 144 | /* 145 | * The varargs behavior of the original API makes this the 146 | * simplest way to get the arguments. 147 | */ 148 | response = arguments[arguments.length - 1]; 149 | args = Array.prototype.slice.call( 150 | arguments, 0, arguments.length - 1); 151 | if (args.length != 1 && typeof (args[0]) != 'object' || 152 | args[0] === null) { 153 | response.end(new Error('bad arguments')); 154 | return; 155 | } 156 | 157 | args = args[0]; 158 | if (!args.hasOwnProperty('echo') || 159 | !Array.isArray(args['echo'])) { 160 | response.end(new Error('expected arg.echo')); 161 | return; 162 | } 163 | 164 | if (typeof (args['delay']) == 'number') { 165 | setTimeout(fastRpcFastbenchFinish, args['delay'], 166 | response, args['echo']); 167 | } else { 168 | fastRpcFastbenchFinish(response, args['echo']); 169 | } 170 | }); 171 | 172 | function fastRpcFastbenchFinish(response, values) { 173 | values.forEach( 174 | function (a) { response.write({ 'value': a }); }); 175 | response.end(); 176 | } 177 | } 178 | 179 | main(); 180 | -------------------------------------------------------------------------------- /test/compat/manual-tst.client_compat.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | * Copyright (c) 2016, Joyent, Inc. 9 | */ 10 | 11 | /* 12 | * test/compat/tst.client_compat.js: tests compatibility of the new client 13 | * against an old server. 14 | */ 15 | 16 | var mod_assertplus = require('assert-plus'); 17 | var mod_bunyan = require('bunyan'); 18 | var mod_cmdutil = require('cmdutil'); 19 | var mod_fast = require('../../lib/fast'); 20 | var mod_path = require('path'); 21 | var mod_net = require('net'); 22 | var mod_vasync = require('vasync'); 23 | var VError = require('verror'); 24 | 25 | var mod_testcommon = require('../common'); 26 | var mod_testcompat = require('./common'); 27 | var testLog, testcases; 28 | 29 | function main() 30 | { 31 | testLog = new mod_bunyan({ 32 | 'name': mod_path.basename(__filename), 33 | 'level': process.env['LOG_LEVEL'] || 'fatal' 34 | }); 35 | 36 | mod_testcommon.registerExitBlocker('tests'); 37 | 38 | mod_testcompat.setupOldServer({ 39 | 'ip': mod_testcommon.serverIp, 40 | 'port': mod_testcommon.serverPort 41 | }, function (err, old) { 42 | if (err) { 43 | mod_cmdutil.fail(err); 44 | } 45 | 46 | mod_vasync.forEachPipeline({ 47 | 'inputs': testcases, 48 | 'func': runTestCase 49 | }, function (err2) { 50 | mod_testcompat.teardownOldServer(old, function () { 51 | if (err2) { 52 | mod_cmdutil.fail(err2); 53 | } 54 | 55 | console.error('%s tests passed', 56 | mod_path.basename(__filename)); 57 | mod_testcommon.unregisterExitBlocker('tests'); 58 | }); 59 | }); 60 | }); 61 | } 62 | 63 | function runTestCase(testcase, callback) 64 | { 65 | var log, csock, cclient; 66 | 67 | console.error('test case: %s', testcase['name']); 68 | log = testLog.child({ 'testcase': testcase['name'] }); 69 | csock = mod_net.createConnection(mod_testcommon.serverPort, 70 | mod_testcommon.serverIp); 71 | cclient = new mod_fast.FastClient({ 72 | 'log': log.child({ 'component': 'FastClient' }), 73 | 'transport': csock, 74 | 'nRecentRequests': 100 75 | }); 76 | 77 | csock.on('connect', function () { 78 | log.info('connected client'); 79 | testcase['run'](log, cclient, function (err) { 80 | if (err) { 81 | console.error('test case "%s" FAILED: %s', 82 | testcase['name'], err.message); 83 | console.error(err.stack); 84 | } 85 | 86 | cclient.detach(); 87 | csock.destroy(); 88 | callback(err); 89 | }); 90 | }); 91 | } 92 | 93 | testcases = [ { 94 | 'name': 'basic RPC, no data', 95 | 'run': function (log, fastclient, callback) { 96 | fastclient.rpcBufferAndCallback({ 97 | 'maxObjectsToBuffer': 0, 98 | 'rpcmethod': 'echo', 99 | 'rpcargs': [ { 100 | 'values': [], 101 | 'errorResult': false 102 | } ] 103 | }, function (err, data, ndata) { 104 | if (!err && ndata !== 0) { 105 | err = new VError('expected 0 data items'); 106 | } 107 | 108 | callback(err); 109 | }); 110 | } 111 | 112 | }, { 113 | 'name': 'basic RPC, some data', 114 | 'run': function (log, fastclient, callback) { 115 | fastclient.rpcBufferAndCallback({ 116 | 'maxObjectsToBuffer': 6, 117 | 'rpcmethod': 'echo', 118 | 'rpcargs': [ { 119 | /* 120 | * null is not allowed by the protocol, but the old server does 121 | * not prevent you from trying to send it 122 | */ 123 | 'values': [ 'one', 'two', false, true, 7, { 'foo': 'bar' } ], 124 | 'errorResult': false 125 | } ] 126 | }, function (err, data, ndata) { 127 | if (err) { 128 | callback(err); 129 | return; 130 | } 131 | 132 | mod_assertplus.equal(data.length, ndata); 133 | mod_assertplus.deepEqual(data, 134 | [ 'one', 'two', false, true, 7, { 'foo': 'bar' } ]); 135 | callback(); 136 | }); 137 | } 138 | 139 | }, { 140 | 'name': 'failed RPC, no data', 141 | 'run': function (log, fastclient, callback) { 142 | fastclient.rpcBufferAndCallback({ 143 | 'maxObjectsToBuffer': 0, 144 | 'rpcmethod': 'echo', 145 | 'rpcargs': [ { 146 | 'values': [], 147 | 'errorResult': true 148 | } ] 149 | }, function (err, data, ndata) { 150 | if (!err) { 151 | callback(new Error('expected error')); 152 | return; 153 | } 154 | 155 | mod_assertplus.equal(data.length, 0); 156 | mod_assertplus.equal(data.length, ndata); 157 | mod_assertplus.equal(err.name, 'FastRequestError'); 158 | err = VError.cause(err); 159 | mod_assertplus.equal(err.name, 'FastServerError'); 160 | err = VError.cause(err); 161 | mod_assertplus.equal(err.name, 'Error'); 162 | mod_assertplus.equal(err.message, 'boom boom!'); 163 | mod_assertplus.deepEqual(err.context, { 'result': 'poof' }); 164 | callback(); 165 | }); 166 | } 167 | 168 | }, { 169 | 'name': 'failed RPC, some data', 170 | 'run': function (log, fastclient, callback) { 171 | fastclient.rpcBufferAndCallback({ 172 | 'maxObjectsToBuffer': 3, 173 | 'rpcmethod': 'echo', 174 | 'rpcargs': [ { 175 | 'values': [ 5, true, 'bob' ], 176 | 'errorResult': true 177 | } ] 178 | }, function (err, data, ndata) { 179 | if (!err) { 180 | callback(new Error('expected error')); 181 | return; 182 | } 183 | 184 | mod_assertplus.equal(data.length, ndata); 185 | mod_assertplus.deepEqual(data, [ 5, true, 'bob' ]); 186 | mod_assertplus.equal(err.name, 'FastRequestError'); 187 | err = VError.cause(err); 188 | mod_assertplus.equal(err.name, 'FastServerError'); 189 | err = VError.cause(err); 190 | mod_assertplus.equal(err.name, 'Error'); 191 | mod_assertplus.equal(err.message, 'boom boom!'); 192 | mod_assertplus.deepEqual(err.context, { 'result': 'poof' }); 193 | callback(); 194 | }); 195 | } 196 | 197 | }, { 198 | 'name': 'working RPC with null value, not allowed', 199 | 'run': function (log, fastclient, callback) { 200 | fastclient.rpcBufferAndCallback({ 201 | 'maxObjectsToBuffer': 3, 202 | 'rpcmethod': 'echo', 203 | 'rpcargs': [ null ] 204 | }, function (err, data, ndata) { 205 | if (!err) { 206 | callback(new Error('expected error')); 207 | return; 208 | } 209 | 210 | /* JSSTYLED */ 211 | mod_assertplus.ok(/server sent "null" value/.test(err.message)); 212 | callback(); 213 | }); 214 | } 215 | 216 | }, { 217 | 'name': 'working RPC with null value, allowed', 218 | 'run': function (log, fastclient, callback) { 219 | fastclient.rpcBufferAndCallback({ 220 | 'maxObjectsToBuffer': 3, 221 | 'ignoreNullValues': true, 222 | 'rpcmethod': 'echo', 223 | 'rpcargs': [ null ] 224 | }, function (err, data, ndata) { 225 | if (err) { 226 | callback(err); 227 | return; 228 | } 229 | 230 | mod_assertplus.equal(data.length, ndata); 231 | mod_assertplus.equal(data.length, 0); 232 | callback(); 233 | }); 234 | } 235 | 236 | } ]; 237 | 238 | main(); 239 | -------------------------------------------------------------------------------- /test/compat/setup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | * Copyright (c) 2016, Joyent, Inc. 9 | */ 10 | 11 | /* 12 | * test/compat/setup.js: command-line tool for setting up a local repository for 13 | * running compatibility tests. This is responsible for installing the legacy 14 | * node-fast package, which requires using a copy of Node 0.10 (while the rest 15 | * of this repo uses 0.12 or later). 16 | */ 17 | 18 | var mod_assertplus = require('assert-plus'); 19 | var mod_child = require('child_process'); 20 | var mod_cmdutil = require('cmdutil'); 21 | var mod_forkexec = require('forkexec'); 22 | var mod_fs = require('fs'); 23 | var mod_vasync = require('vasync'); 24 | 25 | var mod_compat = require('./common'); 26 | 27 | /* 28 | * We test against the Fast server that's widely deployed in Moray, which is 29 | * 0.3.1. 30 | */ 31 | var FAST_MODULE_NAME = 'fast'; 32 | var FAST_MODULE_VERSION = '0.3.1'; 33 | 34 | function main() 35 | { 36 | var npmbin; 37 | var pkgtoinstall = FAST_MODULE_NAME + '@' + FAST_MODULE_VERSION; 38 | 39 | mod_vasync.waterfall([ 40 | function loadConfig(next) { 41 | mod_compat.nodeConfigLoad(next); 42 | }, 43 | 44 | function workaroundNpmMisdesign(nodeconfig, next) { 45 | mod_assertplus.object(nodeconfig); 46 | mod_assertplus.string(nodeconfig.nodebin); 47 | mod_assertplus.string(nodeconfig.npmbin); 48 | npmbin = nodeconfig.npmbin; 49 | 50 | /* 51 | * Contrary to popular belief, npm does not necessarily install 52 | * non-global packages into the current directory. If there's 53 | * no node_modules or package.json there, it walks up to the 54 | * nearest parent directory that has one "even if you happen to 55 | * have cd'ed into some other folder". That's decidedly not 56 | * what we want here, but we can trick it by creating our own 57 | * node_modules directory here. Of course, if it already 58 | * exists, we should do nothing. 59 | */ 60 | mod_fs.mkdir('node_modules', function (err) { 61 | if (err && err['code'] == 'EEXIST') { 62 | err = null; 63 | } 64 | 65 | next(err); 66 | }); 67 | }, 68 | 69 | function installOldModule(next) { 70 | process.stderr.write('installing legacy fast version ' + 71 | pkgtoinstall + ' ... '); 72 | mod_forkexec.forkExecWait({ 73 | 'argv': [ npmbin, 'install', pkgtoinstall ] 74 | }, function (err, info) { 75 | if (err) { 76 | process.stderr.write('FAIL\n'); 77 | next(err); 78 | return; 79 | } 80 | 81 | process.stderr.write('done.\n'); 82 | next(); 83 | }); 84 | } 85 | ], function (err) { 86 | if (err) { 87 | mod_cmdutil.fail(err); 88 | } 89 | 90 | console.log('setup for compatibility tests'); 91 | }); 92 | } 93 | 94 | main(); 95 | -------------------------------------------------------------------------------- /test/tst.allocator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | * Copyright (c) 2016, Joyent, Inc. 9 | */ 10 | 11 | /* 12 | * test/tst.allocator.js: tests our cheesy IdAllocator 13 | */ 14 | 15 | var mod_assertplus = require('assert-plus'); 16 | var mod_subr = require('../lib/subr'); 17 | 18 | var allocator, isAllocated, nQueries; 19 | 20 | console.log('test cases: bad arguments'); 21 | mod_assertplus.throws(function () { 22 | allocator = mod_subr.IdAllocator({}); 23 | }, /args\.min \(number\) is required/); 24 | 25 | mod_assertplus.throws(function () { 26 | allocator = mod_subr.IdAllocator({ 'min': 7 }); 27 | }, /args\.max \(number\) is required/); 28 | 29 | mod_assertplus.throws(function () { 30 | allocator = mod_subr.IdAllocator({ 'min': 7, 'max': 15 }); 31 | }, /args\.isAllocated \(func\) is required/); 32 | 33 | mod_assertplus.throws(function () { 34 | allocator = mod_subr.IdAllocator({ 'min': 18, 'max': 15, 35 | 'isAllocated': function () {} }); 36 | }, /min must be less than max/); 37 | 38 | mod_assertplus.throws(function () { 39 | allocator = mod_subr.IdAllocator({ 'min': -1, 'max': 15, 40 | 'isAllocated': function () {} }); 41 | }, /min must be non-negative/); 42 | 43 | mod_assertplus.throws(function () { 44 | allocator = mod_subr.IdAllocator({ 'min': 0, 'max': Math.pow(2, 37), 45 | 'isAllocated': function () {} }); 46 | }, /max is too big/); 47 | 48 | 49 | console.log('test cases: basic allocator wraps around'); 50 | isAllocated = {}; 51 | nQueries = 0; 52 | allocator = new mod_subr.IdAllocator({ 53 | 'min': 0, 54 | 'max': 7, 55 | 'isAllocated': function (id) { 56 | nQueries++; 57 | return (isAllocated[id]); 58 | } 59 | }); 60 | 61 | /* 62 | * For the first round, we're going to continue acting like nothing is allocated 63 | * to make sure that we wrap around the id space. 64 | */ 65 | mod_assertplus.equal(allocator.alloc(), 0); 66 | mod_assertplus.equal(nQueries, 1); 67 | mod_assertplus.equal(allocator.alloc(), 1); 68 | mod_assertplus.equal(nQueries, 2); 69 | mod_assertplus.equal(allocator.alloc(), 2); 70 | mod_assertplus.equal(nQueries, 3); 71 | mod_assertplus.equal(allocator.alloc(), 3); 72 | mod_assertplus.equal(allocator.alloc(), 4); 73 | mod_assertplus.equal(allocator.alloc(), 5); 74 | mod_assertplus.equal(allocator.alloc(), 6); 75 | mod_assertplus.equal(allocator.alloc(), 7); 76 | mod_assertplus.equal(nQueries, 8); 77 | 78 | /* 79 | * Again, since we're pretending like nothing is allocated, the next round 80 | * should wrap around again as normal. 81 | */ 82 | mod_assertplus.equal(allocator.alloc(), 0); 83 | mod_assertplus.equal(nQueries, 9); 84 | mod_assertplus.equal(allocator.alloc(), 1); 85 | mod_assertplus.equal(allocator.alloc(), 2); 86 | mod_assertplus.equal(allocator.alloc(), 3); 87 | mod_assertplus.equal(allocator.alloc(), 4); 88 | mod_assertplus.equal(allocator.alloc(), 5); 89 | mod_assertplus.equal(allocator.alloc(), 6); 90 | mod_assertplus.equal(allocator.alloc(), 7); 91 | mod_assertplus.equal(allocator.alloc(), 0); 92 | mod_assertplus.equal(allocator.alloc(), 1); 93 | mod_assertplus.equal(nQueries, 18); 94 | 95 | /* 96 | * Now let's pretend like the next several are allocated. Those should be 97 | * skipped, and we should get "5" after asking four questions. 98 | */ 99 | console.log('test cases: allocator skips allocated ids'); 100 | isAllocated[2] = true; 101 | isAllocated[3] = true; 102 | isAllocated[4] = true; 103 | mod_assertplus.equal(allocator.alloc(), 5); 104 | mod_assertplus.equal(nQueries, 22); 105 | 106 | 107 | console.log('test cases: allocator fails if everything is allocated'); 108 | isAllocated[0] = true; 109 | isAllocated[1] = true; 110 | isAllocated[5] = true; 111 | isAllocated[6] = true; 112 | isAllocated[7] = true; 113 | mod_assertplus.throws(function () { allocator.alloc(); }, 114 | /all ids allocated/); 115 | mod_assertplus.equal(nQueries, 30); 116 | 117 | console.log('tst.allocator.js tests passed'); 118 | -------------------------------------------------------------------------------- /test/tst.client_request.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | * Copyright 2020 Joyent, Inc. 9 | */ 10 | 11 | /* 12 | * test/tst.client_request.js: client API per-request test suite 13 | * 14 | * This file contains a test runner (runTestCase) that executes test cases 15 | * following a very prescribed form, where a client connects to the server, the 16 | * server handles the request (often in surprising ways), and the client reports 17 | * some number of errors. This allows us to exercise a lot of different cases, 18 | * including normal, valid RPC calls, RPC calls that return lots of data, and 19 | * various edge cases like invalid messages and unexpected end-of-stream events. 20 | * There are some test cases that require more control over client-server 21 | * interaction. These are defined in tst.client_generic.js. 22 | */ 23 | 24 | var mod_artedi = require('artedi'); 25 | var mod_assertplus = require('assert-plus'); 26 | var mod_bunyan = require('bunyan'); 27 | var mod_microtime = require('microtime'); 28 | var mod_net = require('net'); 29 | var mod_path = require('path'); 30 | var mod_vasync = require('vasync'); 31 | var VError = require('verror'); 32 | 33 | var mod_client = require('../lib/fast_client'); 34 | var mod_protocol = require('../lib/fast_protocol'); 35 | var mod_testclient = require('./common/client'); 36 | var mod_testcommon = require('./common'); 37 | 38 | var testLog; 39 | var startUts; 40 | var serverSocket; 41 | var serverPort = mod_testcommon.serverPort; 42 | var serverIp = mod_testcommon.serverIp; 43 | 44 | function main() 45 | { 46 | testLog = new mod_bunyan({ 47 | 'name': mod_path.basename(__filename), 48 | 'level': process.env['LOG_LEVEL'] || 'fatal' 49 | }); 50 | 51 | startUts = mod_microtime.now(); 52 | 53 | mod_testcommon.registerExitBlocker('test run'); 54 | mod_testcommon.mockServerSetup(function (s) { 55 | testLog.info('server listening'); 56 | serverSocket = s; 57 | 58 | mod_vasync.forEachPipeline({ 59 | 'inputs': mockResponders, 60 | 'func': runTestCase 61 | }, function (err) { 62 | if (err) { 63 | throw (err); 64 | } 65 | 66 | mod_testcommon.unregisterExitBlocker('test run'); 67 | mod_testcommon.mockServerTeardown(serverSocket); 68 | console.log('%s tests passed', 69 | mod_path.basename(__filename)); 70 | }); 71 | }); 72 | } 73 | 74 | function runTestCase(testcase, callback) { 75 | var ctc, ctr; 76 | var collector; 77 | 78 | collector = mod_artedi.createCollector({labels: { 79 | service: 'tst.client_request' 80 | }}); 81 | 82 | console.log('test case: %s', testcase.name); 83 | ctc = new mod_testclient.ClientTestContext({ 84 | 'collector': collector, 85 | 'server': serverSocket, 86 | 'log': testLog.child({ 'testcase': testcase['name'] }) 87 | }); 88 | 89 | ctc.establishConnection(); 90 | ctc.ctc_server_decoder.once('data', function (message) { 91 | ctc.ctc_server_message = message; 92 | testcase['serverReply'](ctc.ctc_server_sock, message, 93 | ctc.ctc_server_encoder, ctc.ctc_server_decoder); 94 | }); 95 | 96 | ctc.ctc_server_decoder.on('error', function (err) { 97 | testcase['serverReply'](ctc.ctc_server_sock, err, 98 | ctc.ctc_server_encoder, ctc.ctc_server_decoder); 99 | }); 100 | 101 | ctr = ctc.makeRequest(function () { 102 | if (testcase.hasOwnProperty('artediPromCheck')) { 103 | /* 104 | * Function provided to check the Prometheus formatted 105 | * metrics. 106 | */ 107 | collector.collect(mod_artedi.FMT_PROM, 108 | function _outputMetrics(err, metrics) { 109 | mod_assertplus.ok(!err); 110 | testcase['artediPromCheck'](metrics); 111 | }); 112 | } 113 | testcase['clientCheck'](ctr.ctr_data, { 114 | 'socket': ctc.ctc_error_sock, 115 | 'client': ctc.ctc_error_client, 116 | 'request': ctr.ctr_error 117 | }); 118 | 119 | ctc.cleanup(); 120 | callback(); 121 | }); 122 | } 123 | 124 | /* 125 | * "mockResponders" describes a bunch of client test cases by describing what 126 | * the server should do and what users of the client API should see when that 127 | * happens. 128 | * 129 | * name a human-readable label for this test case 130 | * 131 | * serverReply a function invoked when the client makes this request. 132 | * This function implements the server's response. The 133 | * function is invoked as: 134 | * 135 | * serverReply(socket, message, encoder) 136 | * 137 | * where 138 | * 139 | * socket the net.Socket connected to the client, which is 140 | * useful for injecting malformed responses or 141 | * generating socket errors 142 | * 143 | * message the first well-formed message read from the socket, 144 | * which is useful for common test cases of responding 145 | * to a basic RPC request 146 | * 147 | * encoder a FastMessageEncoder connected to the socket, which 148 | * is convenient for sending well-formed responses. 149 | * 150 | * decoder a FastMessageDecoder connected to the socket, which 151 | * is convenient for receiving well-formed messages 152 | * 153 | * clientCheck a function invoked after the test case has completed in 154 | * order to verify client behavior. It's invoked as: 155 | * 156 | * clientCheck(data, error) 157 | * 158 | * where 159 | * 160 | * data an array of "data" events emitted by the client 161 | * 162 | * errors an object with properties for each possible error 163 | * emitted during the test, including "socket", "client", 164 | * and "request". 165 | * 166 | * artediPromCheck an optional function invoked after the test case has 167 | * completed in order to verify the Prometheus-formatted 168 | * artedi metrics for this request. It is invoked as: 169 | * 170 | * artediPromCheck(metrics) 171 | * 172 | * where 173 | * 174 | * metrics is a multi-line string containing the metrics for 175 | * this request. 176 | */ 177 | 178 | var mockResponders = [ { 179 | 'name': 'ok, no data', 180 | 'serverReply': function (socket, message, encoder) { 181 | assertNormalRequest(message); 182 | encoder.end({ 183 | 'msgid': message.msgid, 184 | 'status': mod_protocol.FP_STATUS_END, 185 | 'data': mod_testcommon.dummyResponseEndEmpty, 186 | 'version': mod_protocol.FP_VERSION_CURRENT 187 | }); 188 | }, 189 | 'clientCheck': function (data, errors) { 190 | mod_assertplus.ok(errors.socket === null); 191 | mod_assertplus.ok(errors.client === null); 192 | mod_assertplus.ok(errors.request === null); 193 | mod_assertplus.equal(data.length, 0); 194 | } 195 | 196 | }, { 197 | 'name': 'ok, with 4 data messages, with 0-4 data objects per message', 198 | 'serverReply': function (socket, message, encoder) { 199 | var nmessages, i, j, d; 200 | 201 | assertNormalRequest(message); 202 | nmessages = 5; 203 | for (i = 0; i < nmessages; i++) { 204 | d = []; 205 | for (j = 0; j < i; j++) { 206 | d.push('string ' + i + '_' + j); 207 | } 208 | encoder.write({ 209 | 'msgid': message.msgid, 210 | 'status': i == nmessages - 1 ? 211 | mod_protocol.FP_STATUS_END : 212 | mod_protocol.FP_STATUS_DATA, 213 | 'data': { 'd': d }, 214 | 'version': mod_protocol.FP_VERSION_CURRENT 215 | }); 216 | } 217 | 218 | encoder.end(); 219 | }, 220 | 'clientCheck': function (data, errors) { 221 | mod_assertplus.ok(errors.socket === null); 222 | mod_assertplus.ok(errors.client === null); 223 | mod_assertplus.ok(errors.request === null); 224 | mod_assertplus.deepEqual(data, [ 225 | 'string 1_0', 'string 2_0', 'string 2_1', 'string 3_0', 226 | 'string 3_1', 'string 3_2', 'string 4_0', 'string 4_1', 227 | 'string 4_2', 'string 4_3' 228 | ]); 229 | } 230 | 231 | }, { 232 | 'name': 'error, no data', 233 | 'serverReply': function (socket, message, encoder) { 234 | assertNormalRequest(message); 235 | encoder.end({ 236 | 'msgid': message.msgid, 237 | 'status': mod_protocol.FP_STATUS_ERROR, 238 | 'data': mod_testcommon.dummyResponseError, 239 | 'version': mod_protocol.FP_VERSION_CURRENT 240 | }); 241 | }, 242 | 'clientCheck': function (data, errors) { 243 | mod_assertplus.ok(errors.socket === null); 244 | mod_assertplus.ok(errors.client === null); 245 | mod_assertplus.ok(errors.request !== null); 246 | mod_assertplus.equal(data.length, 0); 247 | assertServerError(errors.request, mod_testcommon.dummyError); 248 | } 249 | 250 | }, { 251 | 'name': 'error, after data', 252 | 'serverReply': function (socket, message, encoder) { 253 | var nmessages, i; 254 | assertNormalRequest(message); 255 | 256 | nmessages = 5; 257 | for (i = 0; i < nmessages; i++) { 258 | encoder.write({ 259 | 'msgid': message.msgid, 260 | 'status': mod_protocol.FP_STATUS_DATA, 261 | 'data': mod_testcommon.dummyResponseData, 262 | 'version': mod_protocol.FP_VERSION_CURRENT 263 | }); 264 | } 265 | 266 | encoder.end({ 267 | 'msgid': message.msgid, 268 | 'status': mod_protocol.FP_STATUS_ERROR, 269 | 'data': mod_testcommon.dummyResponseError, 270 | 'version': mod_protocol.FP_VERSION_CURRENT 271 | }); 272 | }, 273 | 'clientCheck': function (data, errors) { 274 | var i; 275 | mod_assertplus.ok(errors.socket === null); 276 | mod_assertplus.ok(errors.client === null); 277 | mod_assertplus.ok(errors.request !== null); 278 | mod_assertplus.equal(data.length, 5); 279 | 280 | for (i = 0; i < data.length; i++) { 281 | mod_assertplus.deepEqual(data[i], mod_testcommon.dummyValue); 282 | } 283 | 284 | assertServerError(errors.request, mod_testcommon.dummyError); 285 | } 286 | 287 | }, { 288 | 'name': 'error with extra properties', 289 | 'serverReply': function (socket, message, encoder) { 290 | /* 291 | * The "context" and "ase_errors" properties are supposed to be 292 | * preserved for historical reasons, but other miscellaneous properties 293 | * do not get transmitted. 294 | */ 295 | assertNormalRequest(message); 296 | encoder.end({ 297 | 'msgid': message.msgid, 298 | 'status': mod_protocol.FP_STATUS_ERROR, 299 | 'data': { 300 | 'd': { 301 | 'name': 'DummyError', 302 | 'message': 'a dummy message', 303 | 'someOtherProp': 'bogus', 304 | 'context': 'abc123', 305 | 'ase_errors': [ 'foobar' ] 306 | } 307 | }, 308 | 'version': mod_protocol.FP_VERSION_CURRENT 309 | }); 310 | }, 311 | 'clientCheck': function (data, errors) { 312 | var error; 313 | 314 | mod_assertplus.ok(errors.socket === null); 315 | mod_assertplus.ok(errors.client === null); 316 | mod_assertplus.ok(errors.request !== null); 317 | mod_assertplus.equal(data.length, 0); 318 | 319 | error = errors.request; 320 | mod_assertplus.equal(error.name, 'FastRequestError'); 321 | error = VError.cause(error); 322 | mod_assertplus.equal(error.name, 'FastServerError'); 323 | error = VError.cause(error); 324 | mod_assertplus.equal(error.name, 'DummyError'); 325 | mod_assertplus.deepEqual(error.context, 'abc123'); 326 | mod_assertplus.deepEqual(error.ase_errors, [ 'foobar' ]); 327 | mod_assertplus.ok(!error.hasOwnProperty('someOtherProp')); 328 | } 329 | 330 | }, { 331 | 'name': 'unexpected end of stream: no response at all', 332 | 'serverReply': function (socket, message, encoder) { 333 | socket.end(); 334 | }, 335 | 'clientCheck': function (data, errors) { 336 | mod_assertplus.ok(errors.socket === null); 337 | mod_assertplus.ok(errors.client !== null); 338 | mod_assertplus.ok(errors.request !== null); 339 | mod_assertplus.equal(data.length, 0); 340 | 341 | mod_assertplus.equal(errors.client.name, 'FastProtocolError'); 342 | mod_assertplus.equal(errors.client.message, 343 | 'unexpected end of transport stream'); 344 | mod_testcommon.assertRequestError(errors.request, errors.client); 345 | } 346 | 347 | }, { 348 | 'name': 'unexpected end of stream: partial message response', 349 | 'serverReply': function (socket, message, encoder) { 350 | var buf = new Buffer(1); 351 | buf.writeUInt8(mod_protocol.FP_VERSION_CURRENT, 352 | mod_protocol.FP_OFF_VERSION); 353 | socket.end(buf); 354 | }, 355 | 'clientCheck': function (data, errors) { 356 | mod_assertplus.ok(errors.socket === null); 357 | mod_assertplus.ok(errors.client !== null); 358 | mod_assertplus.ok(errors.request !== null); 359 | mod_assertplus.equal(data.length, 0); 360 | 361 | mod_assertplus.equal(errors.client.name, 'FastProtocolError'); 362 | mod_assertplus.equal(errors.client.message, 363 | 'fast protocol: incomplete message at end-of-stream'); 364 | mod_testcommon.assertRequestError(errors.request, errors.client); 365 | } 366 | 367 | }, { 368 | 'name': 'unexpected end of stream: complete DATA message, no END', 369 | 'serverReply': function (socket, message, encoder) { 370 | encoder.end({ 371 | 'msgid': message.msgid, 372 | 'status': mod_protocol.FP_STATUS_DATA, 373 | 'data': mod_testcommon.dummyResponseData, 374 | 'version': mod_protocol.FP_VERSION_CURRENT 375 | }); 376 | }, 377 | 'clientCheck': function (data, errors) { 378 | mod_assertplus.ok(errors.socket === null); 379 | mod_assertplus.ok(errors.client !== null); 380 | mod_assertplus.ok(errors.request !== null); 381 | mod_assertplus.equal(data.length, 1); 382 | mod_assertplus.deepEqual(data[0], mod_testcommon.dummyValue); 383 | 384 | mod_assertplus.equal(errors.client.name, 'FastProtocolError'); 385 | mod_assertplus.equal(errors.client.message, 386 | 'unexpected end of transport stream'); 387 | mod_testcommon.assertRequestError(errors.request, errors.client); 388 | } 389 | 390 | }, { 391 | 'name': 'server responds with wrong msgid', 392 | 'serverReply': function (socket, message, encoder) { 393 | mod_assertplus.ok(message.msgid != 47); 394 | encoder.end({ 395 | 'msgid': 47, 396 | 'status': mod_protocol.FP_STATUS_END, 397 | 'data': mod_testcommon.dummyResponseData, 398 | 'version': mod_protocol.FP_VERSION_CURRENT 399 | }); 400 | }, 401 | 'clientCheck': function (data, errors) { 402 | mod_assertplus.ok(errors.socket === null); 403 | mod_assertplus.ok(errors.client !== null); 404 | mod_assertplus.ok(errors.request !== null); 405 | mod_assertplus.equal(data.length, 0); 406 | 407 | mod_assertplus.equal(errors.client.name, 'FastProtocolError'); 408 | mod_assertplus.equal(errors.client.message, 409 | 'fast protocol: received message with unknown msgid 47'); 410 | mod_testcommon.assertRequestError(errors.request, errors.client); 411 | } 412 | 413 | }, { 414 | 'name': 'server responds with invalid message', 415 | 'serverReply': function (socket, message, encoder) { 416 | /* 417 | * This test case exercises client handling of all decoder errors. The 418 | * various decoder failure modes are tested separately in 419 | * ./test/tst.protocol_decoder.js. 420 | */ 421 | var buf = new Buffer(mod_protocol.FP_HEADER_SZ + 1); 422 | mod_testcommon.writeMessageForEncodedData( 423 | buf, 3, mod_protocol.FP_STATUS_END, '{', 0); 424 | socket.end(buf); 425 | }, 426 | 'clientCheck': function (data, errors) { 427 | mod_assertplus.ok(errors.socket === null); 428 | mod_assertplus.ok(errors.client !== null); 429 | mod_assertplus.ok(errors.request !== null); 430 | mod_assertplus.equal(data.length, 0); 431 | 432 | mod_assertplus.equal(errors.client.name, 'FastProtocolError'); 433 | mod_assertplus.ok(/fast protocol: invalid JSON/.test( 434 | errors.client.message)); 435 | mod_testcommon.assertRequestError(errors.request, errors.client); 436 | } 437 | 438 | }, { 439 | 'name': 'server responds with invalid error', 440 | 'serverReply': function (socket, message, encoder) { 441 | encoder.end({ 442 | 'msgid': message.msgid, 443 | 'status': mod_protocol.FP_STATUS_ERROR, 444 | 'data': { 'd': {} }, 445 | 'version': mod_protocol.FP_VERSION_CURRENT 446 | }); 447 | }, 448 | 'clientCheck': function (data, errors) { 449 | mod_assertplus.ok(errors.socket === null); 450 | mod_assertplus.ok(errors.client !== null); 451 | mod_assertplus.ok(errors.request !== null); 452 | mod_assertplus.equal(data.length, 0); 453 | 454 | mod_assertplus.equal(errors.client.name, 'FastProtocolError'); 455 | mod_assertplus.ok(/data.d for ERROR messages must have name/.test( 456 | errors.client.message)); 457 | mod_testcommon.assertRequestError(errors.request, errors.client); 458 | } 459 | 460 | }, { 461 | 'name': 'ok, with 10,000 data messages', 462 | 'serverReply': function (socket, message, encoder) { 463 | var nmessages, i; 464 | 465 | assertNormalRequest(message); 466 | nmessages = 10000; 467 | for (i = 0; i < nmessages; i++) { 468 | encoder.write({ 469 | 'msgid': message.msgid, 470 | 'status': mod_protocol.FP_STATUS_DATA, 471 | 'data': { 'd': [ 'string_' + i ] }, 472 | 'version': mod_protocol.FP_VERSION_CURRENT 473 | }); 474 | } 475 | 476 | encoder.end({ 477 | 'msgid': message.msgid, 478 | 'status': mod_protocol.FP_STATUS_END, 479 | 'data': { 'd': [ 'lastmessage' ] }, 480 | 'version': mod_protocol.FP_VERSION_CURRENT 481 | }); 482 | }, 483 | 'clientCheck': function (data, errors) { 484 | var i; 485 | mod_assertplus.ok(errors.socket === null); 486 | mod_assertplus.ok(errors.client === null); 487 | mod_assertplus.ok(errors.request === null); 488 | mod_assertplus.equal(data.length, 10001); 489 | 490 | for (i = 0; i < data.length - 1; i++) { 491 | mod_assertplus.equal(data[i], 'string_' + i); 492 | } 493 | 494 | mod_assertplus.equal(data[data.length - 1], 'lastmessage'); 495 | } 496 | 497 | }, { 498 | 'name': 'ok, with 10,000 items in an END message', 499 | 'serverReply': function (socket, message, encoder) { 500 | var nitems, d, i; 501 | 502 | assertNormalRequest(message); 503 | nitems = 10000; 504 | d = []; 505 | for (i = 0; i < nitems; i++) { 506 | d.push('string_' + i); 507 | } 508 | 509 | encoder.end({ 510 | 'msgid': message.msgid, 511 | 'status': mod_protocol.FP_STATUS_END, 512 | 'data': { 'd': d }, 513 | 'version': mod_protocol.FP_VERSION_CURRENT 514 | }); 515 | }, 516 | 'clientCheck': function (data, errors) { 517 | var i; 518 | mod_assertplus.ok(errors.socket === null); 519 | mod_assertplus.ok(errors.client === null); 520 | mod_assertplus.ok(errors.request === null); 521 | mod_assertplus.equal(data.length, 10000); 522 | 523 | for (i = 0; i < data.length; i++) { 524 | mod_assertplus.equal(data[i], 'string_' + i); 525 | } 526 | } 527 | 528 | }, { 529 | 'name': 'artedi metrics ok for simple request', 530 | 'serverReply': function (socket, message, encoder) { 531 | var d = ['hello world']; 532 | 533 | assertNormalRequest(message); 534 | encoder.write({ 535 | 'msgid': message.msgid, 536 | 'status': mod_protocol.FP_STATUS_END, 537 | 'data': { 'd': d }, 538 | 'version': mod_protocol.FP_VERSION_CURRENT 539 | }); 540 | 541 | encoder.end(); 542 | }, 543 | 'artediPromCheck': function (metrics) { 544 | var metricsLines; 545 | mod_assertplus.string(metrics); 546 | mod_assertplus.ok(metrics.length > 0); 547 | 548 | metricsLines = metrics.trim().split(/\n/); 549 | mod_assertplus.ok(metricsLines.indexOf( 550 | 'fast_client_requests_completed{rpcMethod="testmethod",' + 551 | 'service="tst.client_request"} 1') !== -1); 552 | }, 553 | 'clientCheck': function (data, errors) { 554 | mod_assertplus.ok(errors.socket === null); 555 | mod_assertplus.ok(errors.client === null); 556 | mod_assertplus.ok(errors.request === null); 557 | mod_assertplus.deepEqual(data, ['hello world']); 558 | } 559 | 560 | } ]; 561 | 562 | /* 563 | * Asserts that the given Fast message represents a well-formed RPC request. 564 | */ 565 | function assertNormalRequest(message) 566 | { 567 | mod_assertplus.equal(message.status, mod_protocol.FP_STATUS_DATA); 568 | mod_assertplus.object(message.data); 569 | mod_assertplus.object(message.data.m); 570 | mod_assertplus.string(message.data.m.name); 571 | mod_assertplus.ok(message.data.m.uts >= startUts); 572 | mod_assertplus.ok(message.data.m.uts <= mod_microtime.now()); 573 | mod_assertplus.array(message.data.d); 574 | } 575 | 576 | /* 577 | * Asserts that the given found_error matches what we would expect if the server 578 | * responded with the given server_error. 579 | */ 580 | function assertServerError(found_error, server_error) 581 | { 582 | /* 583 | * Our current behavior is extremely pedantic, but at least it's clear 584 | * what really happened in all cases. 585 | */ 586 | var cause, info; 587 | 588 | mod_assertplus.equal(found_error.name, 'FastRequestError'); 589 | mod_assertplus.equal(found_error.message, 590 | 'request failed: server error: ' + server_error.message); 591 | 592 | cause = found_error.cause(); 593 | mod_assertplus.equal(cause.name, 'FastServerError'); 594 | mod_assertplus.equal(cause.message, 595 | 'server error: ' + server_error.message); 596 | 597 | cause = cause.cause(); 598 | mod_assertplus.equal(cause.name, server_error.name); 599 | mod_assertplus.equal(cause.message, server_error.message); 600 | 601 | info = VError.info(found_error); 602 | mod_assertplus.number(info['rpcMsgid'], 1); 603 | mod_assertplus.equal(info['rpcMethod'], 604 | mod_testcommon.dummyRpcMethodName); 605 | mod_assertplus.equal(info['dummyProp'], 'dummyVal'); 606 | } 607 | 608 | main(); 609 | -------------------------------------------------------------------------------- /test/tst.protocol_decoder.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | * Copyright 2020 Joyent, Inc. 9 | */ 10 | 11 | /* 12 | * test/tst.protocol_decoder.js: fast protocol decoder tests 13 | */ 14 | 15 | var mod_assertplus = require('assert-plus'); 16 | var mod_cmdutil = require('cmdutil'); 17 | var mod_old_crc = require('oldcrc'); 18 | var mod_crc = require('crc'); 19 | var mod_extsprintf = require('extsprintf'); 20 | var mod_path = require('path'); 21 | var mod_vasync = require('vasync'); 22 | var VError = require('verror'); 23 | 24 | var mod_protocol = require('../lib/fast_protocol'); 25 | var printf = mod_extsprintf.printf; 26 | 27 | var mod_testcommon = require('./common'); 28 | 29 | var test_cases; 30 | 31 | function main() 32 | { 33 | mod_testcommon.registerExitBlocker('test run'); 34 | mod_vasync.forEachPipeline({ 35 | 'inputs': test_cases, 36 | 'func': runTestCase 37 | }, function (err) { 38 | if (err) { 39 | throw (err); 40 | } 41 | 42 | mod_testcommon.unregisterExitBlocker('test run'); 43 | printf('%s tests passed\n', mod_path.basename(__filename)); 44 | }); 45 | } 46 | 47 | function runTestCase(testcase, callback) 48 | { 49 | var decoder = new mod_protocol.FastMessageDecoder(); 50 | var data = []; 51 | var error = null; 52 | 53 | printf('test case: %s: ', testcase['name']); 54 | 55 | decoder.on('data', function (c) { data.push(c); }); 56 | decoder.on('error', function (err) { 57 | mod_assertplus.ok(error === null); 58 | error = err; 59 | testcase['check'](error, data); 60 | printf('ok\n'); 61 | callback(); 62 | }); 63 | decoder.on('end', function () { 64 | mod_assertplus.ok(error === null); 65 | testcase['check'](error, data); 66 | printf('ok\n'); 67 | callback(); 68 | }); 69 | 70 | decoder.end(testcase['input']()); 71 | } 72 | 73 | var sample_object = { 'd': [ { 'hello': 'world' } ] }; 74 | var sample_data = JSON.stringify(sample_object); 75 | var sample_old_crc = mod_old_crc.crc16(sample_data); 76 | var sample_crc = mod_crc.crc16(sample_data); 77 | 78 | var sample_error = { 'd': { 'name': 'AnError', 'message': 'boom!' } }; 79 | 80 | /* This object winds up being about 28MB encoded as JSON. */ 81 | var big_object = { 'd': [ mod_testcommon.makeBigObject(10, 6) ] }; 82 | var big_data = JSON.stringify(big_object); 83 | var big_crc = mod_old_crc.crc16(big_data); 84 | 85 | test_cases = [ { 86 | 'name': 'basic DATA message, protocol version 1 (old CRC calculaton)', 87 | 'input': function () { 88 | /* 89 | * The first few of these test cases hardcode protocol values to make 90 | * sure these constants don't break silently on us (e.g., checking null 91 | * against undefined because FP_OFF_TYPE has been deleted). Later, 92 | * we'll just use the constants for clarity. 93 | */ 94 | var buf = new Buffer(mod_protocol.FP_HEADER_SZ + sample_data.length); 95 | buf.writeUInt8(mod_protocol.FP_VERSION_1, mod_protocol.FP_OFF_VERSION); 96 | buf.writeUInt8(0x1, mod_protocol.FP_OFF_TYPE); 97 | buf.writeUInt8(0x1, mod_protocol.FP_OFF_STATUS); 98 | buf.writeUInt32BE(0xbadcafe, mod_protocol.FP_OFF_MSGID); 99 | buf.writeUInt32BE(sample_old_crc, mod_protocol.FP_OFF_CRC); 100 | buf.writeUInt32BE(sample_data.length, mod_protocol.FP_OFF_DATALEN); 101 | buf.write(sample_data, mod_protocol.FP_OFF_DATA); 102 | return (buf); 103 | }, 104 | 'check': function (error, data) { 105 | mod_assertplus.ok(error === null); 106 | mod_assertplus.equal(data.length, 1); 107 | mod_assertplus.equal(data[0].msgid, 0xbadcafe); 108 | mod_assertplus.equal(data[0].status, mod_protocol.FP_STATUS_DATA); 109 | mod_assertplus.deepEqual(data[0].data, sample_object); 110 | } 111 | }, { 112 | 'name': 'basic DATA message, current protocol version', 113 | 'input': function () { 114 | /* 115 | * The first few of these test cases hardcode protocol values to make 116 | * sure these constants don't break silently on us (e.g., checking null 117 | * against undefined because FP_OFF_TYPE has been deleted). Later, 118 | * we'll just use the constants for clarity. 119 | */ 120 | var buf = new Buffer(mod_protocol.FP_HEADER_SZ + sample_data.length); 121 | buf.writeUInt8(mod_protocol.FP_VERSION_CURRENT, 122 | mod_protocol.FP_OFF_VERSION); 123 | buf.writeUInt8(0x1, mod_protocol.FP_OFF_TYPE); 124 | buf.writeUInt8(0x1, mod_protocol.FP_OFF_STATUS); 125 | buf.writeUInt32BE(0xbadcafe, mod_protocol.FP_OFF_MSGID); 126 | buf.writeUInt32BE(sample_crc, mod_protocol.FP_OFF_CRC); 127 | buf.writeUInt32BE(sample_data.length, mod_protocol.FP_OFF_DATALEN); 128 | buf.write(sample_data, mod_protocol.FP_OFF_DATA); 129 | return (buf); 130 | }, 131 | 'check': function (error, data) { 132 | mod_assertplus.ok(error === null); 133 | mod_assertplus.equal(data.length, 1); 134 | mod_assertplus.equal(data[0].msgid, 0xbadcafe); 135 | mod_assertplus.equal(data[0].status, mod_protocol.FP_STATUS_DATA); 136 | mod_assertplus.deepEqual(data[0].data, sample_object); 137 | } 138 | }, { 139 | 'name': 'large END message', 140 | 'input': function () { 141 | return (makeMessageForData( 142 | 14, mod_protocol.FP_STATUS_END, big_object)); 143 | }, 144 | 'check': function (error, data) { 145 | mod_assertplus.ok(error === null); 146 | mod_assertplus.equal(data.length, 1); 147 | mod_assertplus.equal(data[0].msgid, 14); 148 | mod_assertplus.equal(data[0].status, mod_protocol.FP_STATUS_END); 149 | mod_assertplus.deepEqual(data[0].data, big_object); 150 | } 151 | }, { 152 | 'name': 'basic ERROR message', 153 | 'input': function () { 154 | return (makeMessageForData(47, 0x3, sample_error)); 155 | }, 156 | 'check': function (error, data) { 157 | mod_assertplus.ok(error === null); 158 | mod_assertplus.equal(data.length, 1); 159 | mod_assertplus.equal(data[0].msgid, 47); 160 | mod_assertplus.equal(data[0].status, mod_protocol.FP_STATUS_ERROR); 161 | mod_assertplus.deepEqual(data[0].data, sample_error); 162 | } 163 | }, { 164 | 'name': 'DATA message with maximum msgid', 165 | 'input': function () { 166 | var buf = makeSampleMessage(); 167 | buf.writeUInt32BE(mod_protocol.FP_MSGID_MAX, mod_protocol.FP_OFF_MSGID); 168 | return (buf); 169 | }, 170 | 'check': function (error, data) { 171 | mod_assertplus.ok(error === null); 172 | mod_assertplus.equal(data.length, 1); 173 | mod_assertplus.equal(data[0].msgid, 2147483647); 174 | mod_assertplus.equal(data[0].status, mod_protocol.FP_STATUS_DATA); 175 | mod_assertplus.deepEqual(data[0].data, sample_object); 176 | } 177 | }, { 178 | 'name': 'empty stream', 179 | 'input': function () { 180 | return (undefined); 181 | }, 182 | 'check': function (error, data) { 183 | mod_assertplus.ok(error === null); 184 | mod_assertplus.equal(data.length, 0); 185 | } 186 | }, { 187 | 'name': '10,000 DATA messages', 188 | 'input': function () { 189 | var nmessages, buf, msgsize, msgoffset, i; 190 | 191 | nmessages = 10000; 192 | msgsize = mod_protocol.FP_HEADER_SZ + sample_data.length; 193 | buf = new Buffer(nmessages * msgsize); 194 | for (i = 0; i < nmessages; i++) { 195 | msgoffset = i * msgsize; 196 | mod_testcommon.writeMessageForEncodedData(buf, i + 1, 197 | mod_protocol.FP_STATUS_DATA, sample_data, msgoffset); 198 | } 199 | 200 | return (buf); 201 | }, 202 | 'check': function (error, data) { 203 | var i; 204 | mod_assertplus.ok(error === null); 205 | mod_assertplus.equal(data.length, 10000); 206 | for (i = 0; i < data.length; i++) { 207 | mod_assertplus.equal(data[i].msgid, i + 1); 208 | mod_assertplus.equal(data[i].status, 209 | mod_protocol.FP_STATUS_DATA); 210 | mod_assertplus.equal(data[i].version, 211 | mod_protocol.FP_VERSION_CURRENT); 212 | mod_assertplus.deepEqual(data[i].data, sample_object); 213 | } 214 | } 215 | }, { 216 | 'name': '10,000 DATA messages, protocol version 1 (old CRC calculaton)', 217 | 'input': function () { 218 | var nmessages, buf, msgsize, msgoffset, i; 219 | 220 | nmessages = 10000; 221 | msgsize = mod_protocol.FP_HEADER_SZ + sample_data.length; 222 | buf = new Buffer(nmessages * msgsize); 223 | for (i = 0; i < nmessages; i++) { 224 | msgoffset = i * msgsize; 225 | mod_testcommon.writeMessageForEncodedData(buf, i + 1, 226 | mod_protocol.FP_STATUS_DATA, sample_data, msgoffset, 227 | mod_protocol.FP_VERSION_1); 228 | } 229 | 230 | return (buf); 231 | }, 232 | 'check': function (error, data) { 233 | var i; 234 | mod_assertplus.ok(error === null); 235 | mod_assertplus.equal(data.length, 10000); 236 | for (i = 0; i < data.length; i++) { 237 | mod_assertplus.equal(data[i].msgid, i + 1); 238 | mod_assertplus.equal(data[i].status, 239 | mod_protocol.FP_STATUS_DATA); 240 | mod_assertplus.equal(data[i].version, 241 | mod_protocol.FP_VERSION_1); 242 | mod_assertplus.deepEqual(data[i].data, sample_object); 243 | } 244 | } 245 | }, { 246 | 'name': '10,000 messages with an error contained', 247 | 'input': function () { 248 | var nmessages, buf, msgsize, msgoffset, i; 249 | 250 | nmessages = 10000; 251 | msgsize = mod_protocol.FP_HEADER_SZ + sample_data.length; 252 | buf = new Buffer(nmessages * msgsize); 253 | for (i = 0; i < nmessages; i++) { 254 | msgoffset = i * msgsize; 255 | mod_testcommon.writeMessageForEncodedData(buf, i + 1, 256 | mod_protocol.FP_STATUS_DATA, sample_data, msgoffset); 257 | if (i == 1000) { 258 | buf.writeUInt8(0x0, 259 | msgoffset + mod_protocol.FP_OFF_VERSION); 260 | } 261 | } 262 | 263 | return (buf); 264 | }, 265 | 'check': function (error, data) { 266 | var i; 267 | mod_assertplus.ok(error !== null); 268 | mod_assertplus.ok(error instanceof Error); 269 | mod_assertplus.equal(error.name, 'FastProtocolError'); 270 | mod_assertplus.ok(/unsupported version 0/.test(error.message)); 271 | mod_assertplus.equal(VError.info(error).fastReason, 272 | 'unsupported_version'); 273 | mod_assertplus.equal(VError.info(error).foundVersion, 0); 274 | 275 | mod_assertplus.equal(data.length, 1000); 276 | for (i = 0; i < data.length; i++) { 277 | mod_assertplus.equal(data[i].msgid, i + 1); 278 | mod_assertplus.equal(data[i].status, 279 | mod_protocol.FP_STATUS_DATA); 280 | mod_assertplus.equal(data[i].version, 281 | mod_protocol.FP_VERSION_CURRENT); 282 | mod_assertplus.deepEqual(data[i].data, sample_object); 283 | } 284 | } 285 | }, { 286 | 'name': 'bad version (0)', 287 | 'input': function () { 288 | var buf = makeSampleMessage(); 289 | buf.writeUInt8(0, mod_protocol.FP_OFF_VERSION); 290 | return (buf); 291 | }, 292 | 'check': function (error, data) { 293 | mod_assertplus.equal(data.length, 0); 294 | mod_assertplus.ok(error instanceof Error); 295 | mod_assertplus.equal(error.name, 'FastProtocolError'); 296 | mod_assertplus.ok(/unsupported version 0/.test(error.message)); 297 | mod_assertplus.equal(VError.info(error).fastReason, 298 | 'unsupported_version'); 299 | mod_assertplus.equal(VError.info(error).foundVersion, 0); 300 | } 301 | }, { 302 | 'name': 'bad version (37)', 303 | 'input': function () { 304 | var buf = makeSampleMessage(); 305 | buf.writeUInt8(37, mod_protocol.FP_OFF_VERSION); 306 | return (buf); 307 | }, 308 | 'check': function (error, data) { 309 | mod_assertplus.equal(data.length, 0); 310 | mod_assertplus.ok(error instanceof Error); 311 | mod_assertplus.equal(error.name, 'FastProtocolError'); 312 | mod_assertplus.ok(/unsupported version 37/.test(error.message)); 313 | mod_assertplus.equal(VError.info(error).fastReason, 314 | 'unsupported_version'); 315 | mod_assertplus.equal(VError.info(error).foundVersion, 37); 316 | } 317 | }, { 318 | 'name': 'bad type (0)', 319 | 'input': function () { 320 | var buf = makeSampleMessage(); 321 | buf.writeUInt8(0, mod_protocol.FP_OFF_TYPE); 322 | return (buf); 323 | }, 324 | 'check': function (error, data) { 325 | mod_assertplus.equal(data.length, 0); 326 | mod_assertplus.ok(error instanceof Error); 327 | mod_assertplus.equal(error.name, 'FastProtocolError'); 328 | mod_assertplus.ok(/unsupported type 0x0/.test(error.message)); 329 | mod_assertplus.equal(VError.info(error).fastReason, 'unsupported_type'); 330 | mod_assertplus.equal(VError.info(error).foundType, 0); 331 | } 332 | }, { 333 | 'name': 'bad type (2)', 334 | 'input': function () { 335 | var buf = makeSampleMessage(); 336 | buf.writeUInt8(2, mod_protocol.FP_OFF_TYPE); 337 | return (buf); 338 | }, 339 | 'check': function (error, data) { 340 | mod_assertplus.equal(data.length, 0); 341 | mod_assertplus.ok(error instanceof Error); 342 | mod_assertplus.equal(error.name, 'FastProtocolError'); 343 | mod_assertplus.ok(/unsupported type 0x2/.test(error.message)); 344 | mod_assertplus.equal(VError.info(error).fastReason, 'unsupported_type'); 345 | mod_assertplus.equal(VError.info(error).foundType, 2); 346 | } 347 | }, { 348 | 'name': 'bad status (0)', 349 | 'input': function () { 350 | var buf = makeSampleMessage(); 351 | buf.writeUInt8(0, mod_protocol.FP_OFF_STATUS); 352 | return (buf); 353 | }, 354 | 'check': function (error, data) { 355 | mod_assertplus.equal(data.length, 0); 356 | mod_assertplus.ok(error instanceof Error); 357 | mod_assertplus.equal(error.name, 'FastProtocolError'); 358 | mod_assertplus.ok(/unsupported status 0x0/.test(error.message)); 359 | mod_assertplus.equal(VError.info(error).fastReason, 360 | 'unsupported_status'); 361 | mod_assertplus.equal(VError.info(error).foundStatus, 0); 362 | } 363 | }, { 364 | 'name': 'bad status (4)', 365 | 'input': function () { 366 | var buf = makeSampleMessage(); 367 | buf.writeUInt8(0x4, mod_protocol.FP_OFF_STATUS); 368 | return (buf); 369 | }, 370 | 'check': function (error, data) { 371 | mod_assertplus.equal(data.length, 0); 372 | mod_assertplus.ok(error instanceof Error); 373 | mod_assertplus.equal(error.name, 'FastProtocolError'); 374 | mod_assertplus.ok(/unsupported status 0x4/.test(error.message)); 375 | mod_assertplus.equal(VError.info(error).fastReason, 376 | 'unsupported_status'); 377 | mod_assertplus.equal(VError.info(error).foundStatus, 4); 378 | } 379 | }, { 380 | 'name': 'bad msgid (too large)', 381 | 'input': function () { 382 | var buf = makeSampleMessage(); 383 | buf.writeUInt32BE(mod_protocol.FP_MSGID_MAX + 1, 384 | mod_protocol.FP_OFF_MSGID); 385 | return (buf); 386 | }, 387 | 'check': function (error, data) { 388 | mod_assertplus.equal(data.length, 0); 389 | mod_assertplus.ok(error instanceof Error); 390 | mod_assertplus.equal(error.name, 'FastProtocolError'); 391 | mod_assertplus.ok(/invalid msgid 2147483648/.test(error.message)); 392 | mod_assertplus.equal(VError.info(error).fastReason, 'invalid_msgid'); 393 | mod_assertplus.equal(VError.info(error).foundMsgid, 2147483648); 394 | } 395 | }, { 396 | 'name': 'bad CRC', 397 | 'input': function () { 398 | var buf = makeSampleMessage(); 399 | mod_assertplus.ok( 400 | buf.readUInt32BE(mod_protocol.FP_OFF_CRC) != 0xdeadbeef); 401 | buf.writeUInt32BE(0xdeadbeef, mod_protocol.FP_OFF_CRC); 402 | return (buf); 403 | }, 404 | 'check': function (error, data) { 405 | mod_assertplus.equal(data.length, 0); 406 | mod_assertplus.ok(error instanceof Error); 407 | mod_assertplus.equal(error.name, 'FastProtocolError'); 408 | mod_assertplus.ok(/expected CRC 3735928559, found/.test(error.message)); 409 | mod_assertplus.equal(VError.info(error).fastReason, 'bad_crc'); 410 | mod_assertplus.equal(VError.info(error).crcCalculated, sample_crc); 411 | mod_assertplus.equal(VError.info(error).crcExpected, 0xdeadbeef); 412 | } 413 | }, { 414 | 'name': 'bad: DATA message with non-array data.d', 415 | 'input': function () { 416 | var data, buf; 417 | data = { 'd': { 'foo': 'bar' } }; 418 | buf = makeMessageForData(3, mod_protocol.FP_STATUS_DATA, data); 419 | return (buf); 420 | }, 421 | 'check': function (error, data) { 422 | mod_assertplus.equal(data.length, 0); 423 | mod_assertplus.ok(error instanceof Error); 424 | mod_assertplus.equal(error.name, 'FastProtocolError'); 425 | mod_assertplus.ok(/data.d for DATA.*must be an array/.test( 426 | error.message)); 427 | mod_assertplus.equal(VError.info(error).fastReason, 'bad_data_d'); 428 | } 429 | }, { 430 | 'name': 'bad: END message with non-array data.d', 431 | 'input': function () { 432 | var data, buf; 433 | data = { 'd': { 'foo': 'bar' } }; 434 | buf = makeMessageForData(3, mod_protocol.FP_STATUS_END, data); 435 | return (buf); 436 | }, 437 | 'check': function (error, data) { 438 | mod_assertplus.equal(data.length, 0); 439 | mod_assertplus.ok(error instanceof Error); 440 | mod_assertplus.equal(error.name, 'FastProtocolError'); 441 | mod_assertplus.ok(/data.d for .*END messages must be an array/.test( 442 | error.message)); 443 | mod_assertplus.equal(VError.info(error).fastReason, 'bad_data_d'); 444 | } 445 | }, { 446 | 'name': 'bad: DATA message with null data', 447 | 'input': function () { 448 | var data, buf; 449 | data = null; 450 | buf = makeMessageForData(3, mod_protocol.FP_STATUS_DATA, data); 451 | return (buf); 452 | }, 453 | 'check': function (error, data) { 454 | mod_assertplus.equal(data.length, 0); 455 | mod_assertplus.ok(error instanceof Error); 456 | mod_assertplus.equal(error.name, 'FastProtocolError'); 457 | mod_assertplus.ok(/message data must be a non-null object/.test( 458 | error.message)); 459 | mod_assertplus.equal(VError.info(error).fastReason, 'bad_data'); 460 | } 461 | }, { 462 | 'name': 'bad: DATA message with string data', 463 | 'input': function () { 464 | var data, buf; 465 | data = 'foobar'; 466 | buf = makeMessageForData(3, mod_protocol.FP_STATUS_DATA, data); 467 | return (buf); 468 | }, 469 | 'check': function (error, data) { 470 | mod_assertplus.equal(data.length, 0); 471 | mod_assertplus.ok(error instanceof Error); 472 | mod_assertplus.equal(error.name, 'FastProtocolError'); 473 | mod_assertplus.ok(/message data must be a non-null object/.test( 474 | error.message)); 475 | mod_assertplus.equal(VError.info(error).fastReason, 'bad_data'); 476 | } 477 | }, { 478 | 'name': 'bad: ERROR message with missing data', 479 | 'input': function () { 480 | return (makeMessageForData(47, mod_protocol.FP_STATUS_ERROR, {})); 481 | }, 482 | 'check': function (error, data) { 483 | mod_assertplus.equal(data.length, 0); 484 | mod_assertplus.ok(error instanceof Error); 485 | mod_assertplus.equal(error.name, 'FastProtocolError'); 486 | mod_assertplus.ok(/data\.d for ERROR messages must have name/.test( 487 | error.message)); 488 | mod_assertplus.equal(VError.info(error).fastReason, 'bad_error'); 489 | } 490 | }, { 491 | 'name': 'bad: ERROR message with null d', 492 | 'input': function () { 493 | return (makeMessageForData(47, mod_protocol.FP_STATUS_ERROR, 494 | { 'd': null })); 495 | }, 496 | 'check': function (error, data) { 497 | mod_assertplus.equal(data.length, 0); 498 | mod_assertplus.ok(error instanceof Error); 499 | mod_assertplus.equal(error.name, 'FastProtocolError'); 500 | mod_assertplus.ok(/data\.d for ERROR messages must have name/.test( 501 | error.message)); 502 | mod_assertplus.equal(VError.info(error).fastReason, 'bad_error'); 503 | } 504 | }, { 505 | 'name': 'bad: ERROR message with bad name', 506 | 'input': function () { 507 | return (makeMessageForData(47, mod_protocol.FP_STATUS_ERROR, 508 | { 'd': { 'name': 47, 'message': 'threeve' } })); 509 | }, 510 | 'check': function (error, data) { 511 | mod_assertplus.equal(data.length, 0); 512 | mod_assertplus.ok(error instanceof Error); 513 | mod_assertplus.equal(error.name, 'FastProtocolError'); 514 | mod_assertplus.ok(/data\.d for ERROR messages must have name/.test( 515 | error.message)); 516 | mod_assertplus.equal(VError.info(error).fastReason, 'bad_error'); 517 | } 518 | }, { 519 | 'name': 'bad: end of stream with 1-byte header', 520 | 'input': function () { 521 | var buf = new Buffer(1); 522 | buf.writeUInt8(mod_protocol.FP_VERSION_1, mod_protocol.FP_OFF_VERSION); 523 | return (buf); 524 | }, 525 | 'check': function (error, data) { 526 | mod_assertplus.ok(error instanceof Error); 527 | mod_assertplus.equal(data.length, 0); 528 | mod_assertplus.equal(error.name, 'FastProtocolError'); 529 | mod_assertplus.ok(/incomplete message at end-of-stream/.test( 530 | error.message)); 531 | mod_assertplus.equal(VError.info(error).fastReason, 532 | 'incomplete_message'); 533 | } 534 | }, { 535 | 'name': 'bad: end of stream with full header and no data', 536 | 'input': function () { 537 | var buf = makeSampleMessage(); 538 | buf = buf.slice(0, mod_protocol.FP_HEADER_SZ); 539 | return (buf); 540 | }, 541 | 'check': function (error, data) { 542 | mod_assertplus.ok(error instanceof Error); 543 | mod_assertplus.equal(data.length, 0); 544 | mod_assertplus.equal(error.name, 'FastProtocolError'); 545 | mod_assertplus.ok(/incomplete message at end-of-stream/.test( 546 | error.message)); 547 | mod_assertplus.equal(VError.info(error).fastReason, 548 | 'incomplete_message'); 549 | } 550 | }, { 551 | 'name': 'bad: end of stream with full header and partial data', 552 | 'input': function () { 553 | var buf = makeSampleMessage(); 554 | buf = buf.slice(0, mod_protocol.FP_HEADER_SZ + 1); 555 | return (buf); 556 | }, 557 | 'check': function (error, data) { 558 | mod_assertplus.ok(error instanceof Error); 559 | mod_assertplus.equal(data.length, 0); 560 | mod_assertplus.equal(error.name, 'FastProtocolError'); 561 | mod_assertplus.ok(/incomplete message at end-of-stream/.test( 562 | error.message)); 563 | mod_assertplus.equal(VError.info(error).fastReason, 564 | 'incomplete_message'); 565 | } 566 | }, { 567 | 'name': 'bad: invalid JSON data payload', 568 | 'input': function () { 569 | var datalen, dataenc, buf; 570 | dataenc = '{ "hello"'; 571 | datalen = Buffer.byteLength(dataenc); 572 | buf = new Buffer(mod_protocol.FP_HEADER_SZ + datalen); 573 | mod_testcommon.writeMessageForEncodedData(buf, 3, 574 | mod_protocol.FP_STATUS_DATA, dataenc, 0); 575 | return (buf); 576 | }, 577 | 'check': function (error, data) { 578 | mod_assertplus.ok(error instanceof Error); 579 | mod_assertplus.equal(data.length, 0); 580 | mod_assertplus.equal(error.name, 'FastProtocolError'); 581 | /* JSSTYLED */ 582 | mod_assertplus.ok(/invalid JSON in "data"/.test(error.message)); 583 | mod_assertplus.equal(VError.info(error).fastReason, 'invalid_json'); 584 | } 585 | }, { 586 | 'name': 'bad: 0-byte payload', 587 | 'input': function () { 588 | var buf; 589 | buf = new Buffer(mod_protocol.FP_HEADER_SZ); 590 | mod_testcommon.writeMessageForEncodedData(buf, 3, 591 | mod_protocol.FP_STATUS_DATA, '', 0); 592 | return (buf); 593 | }, 594 | 'check': function (error, data) { 595 | mod_assertplus.ok(error instanceof Error); 596 | mod_assertplus.equal(data.length, 0); 597 | mod_assertplus.equal(error.name, 'FastProtocolError'); 598 | /* JSSTYLED */ 599 | mod_assertplus.ok(/invalid JSON in "data"/.test(error.message)); 600 | mod_assertplus.equal(VError.info(error).fastReason, 'invalid_json'); 601 | } 602 | } ]; 603 | 604 | function makeSampleMessage() 605 | { 606 | return (makeMessageForData(mod_protocol.FP_MSGID_MAX, 607 | mod_protocol.FP_STATUS_DATA, sample_object)); 608 | } 609 | 610 | function makeMessageForData(msgid, status, data) 611 | { 612 | var datalen, dataenc, buf; 613 | 614 | mod_assertplus.number(msgid); 615 | mod_assertplus.number(status); 616 | dataenc = JSON.stringify(data); 617 | datalen = Buffer.byteLength(dataenc); 618 | buf = new Buffer(mod_protocol.FP_HEADER_SZ + datalen); 619 | mod_testcommon.writeMessageForEncodedData( 620 | buf, msgid, status, dataenc, 0); 621 | return (buf); 622 | } 623 | 624 | main(); 625 | -------------------------------------------------------------------------------- /test/tst.protocol_encoder.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | * Copyright 2020 Joyent, Inc. 9 | */ 10 | 11 | /* 12 | * test/tst.protocol_encoder.js: fast protocol encoder tests 13 | */ 14 | 15 | var mod_assertplus = require('assert-plus'); 16 | var mod_cmdutil = require('cmdutil'); 17 | var mod_extsprintf = require('extsprintf'); 18 | var mod_path = require('path'); 19 | 20 | var mod_protocol = require('../lib/fast_protocol'); 21 | var printf = mod_extsprintf.printf; 22 | 23 | var mod_testcommon = require('./common'); 24 | 25 | var bigdata, bigdataval, test_cases; 26 | var circular = {}; 27 | circular['a'] = 47; 28 | circular['b'] = circular; 29 | 30 | function main() 31 | { 32 | /* This object winds up being about 28MB encoded as JSON. */ 33 | printf('generating large object ... '); 34 | bigdata = [ mod_testcommon.makeBigObject(10, 6) ]; 35 | bigdataval = JSON.stringify(bigdata); 36 | printf('%d bytes (stringified)\n', bigdataval.length); 37 | 38 | test_cases.forEach(runTestCase); 39 | printf('%s tests passed\n', mod_path.basename(__filename)); 40 | } 41 | 42 | test_cases = [ { 43 | 'name': 'basic data message', 44 | 'input': { 45 | 'msgid': 1, 46 | 'status': mod_protocol.FP_STATUS_DATA, 47 | 'data': [ 'hello', 'world' ], 48 | 'version': mod_protocol.FP_VERSION_CURRENT 49 | }, 50 | 'check': function (output, parsed) { 51 | var expected = '["hello","world"]'; 52 | var expectedlen = Buffer.byteLength(expected); 53 | mod_assertplus.equal(parsed.pm_datalen, expectedlen); 54 | mod_assertplus.equal(parsed.pm_data.toString('utf8'), expected); 55 | mod_assertplus.equal(parsed.pm_msgid, 1); 56 | mod_assertplus.equal(parsed.pm_crc, 7500); 57 | mod_assertplus.equal(parsed.pm_version, 58 | mod_protocol.FP_VERSION_CURRENT); 59 | } 60 | }, { 61 | 'name': 'basic data message, protocol version 1', 62 | 'input': { 63 | 'msgid': 1, 64 | 'status': mod_protocol.FP_STATUS_DATA, 65 | 'data': [ 'hello', 'world' ], 66 | 'version': mod_protocol.FP_VERSION_1 67 | }, 68 | 'check': function (output, parsed) { 69 | var expected = '["hello","world"]'; 70 | var expectedlen = Buffer.byteLength(expected); 71 | mod_assertplus.equal(parsed.pm_datalen, expectedlen); 72 | mod_assertplus.equal(parsed.pm_data.toString('utf8'), expected); 73 | mod_assertplus.equal(parsed.pm_msgid, 1); 74 | mod_assertplus.equal(parsed.pm_crc, 10980); 75 | mod_assertplus.equal(parsed.pm_version, 76 | mod_protocol.FP_VERSION_1); 77 | } 78 | }, { 79 | 'name': 'large data message', 80 | 'input': { 81 | 'msgid': 7, 82 | 'status': mod_protocol.FP_STATUS_DATA, 83 | 'data': function () { return (bigdata); }, 84 | 'version': mod_protocol.FP_VERSION_CURRENT 85 | }, 86 | 'check': function (output, parsed) { 87 | var expected = bigdataval; 88 | var expectedlen = Buffer.byteLength(expected); 89 | mod_assertplus.equal(parsed.pm_datalen, expectedlen); 90 | mod_assertplus.equal(parsed.pm_data.toString('utf8'), expected); 91 | mod_assertplus.equal(parsed.pm_msgid, 7); 92 | mod_assertplus.equal(parsed.pm_status, mod_protocol.FP_STATUS_DATA); 93 | mod_assertplus.equal(parsed.pm_version, 94 | mod_protocol.FP_VERSION_CURRENT); 95 | } 96 | }, { 97 | 'name': 'large data message, protocol version 1', 98 | 'input': { 99 | 'msgid': 7, 100 | 'status': mod_protocol.FP_STATUS_DATA, 101 | 'data': function () { return (bigdata); }, 102 | 'version': mod_protocol.FP_VERSION_1 103 | }, 104 | 'check': function (output, parsed) { 105 | var expected = bigdataval; 106 | var expectedlen = Buffer.byteLength(expected); 107 | mod_assertplus.equal(parsed.pm_datalen, expectedlen); 108 | mod_assertplus.equal(parsed.pm_data.toString('utf8'), expected); 109 | mod_assertplus.equal(parsed.pm_msgid, 7); 110 | mod_assertplus.equal(parsed.pm_status, mod_protocol.FP_STATUS_DATA); 111 | mod_assertplus.equal(parsed.pm_version, 112 | mod_protocol.FP_VERSION_1); 113 | } 114 | }, { 115 | 'name': 'minimum msgid', 116 | 'input': { 117 | 'msgid': 0, 118 | 'status': mod_protocol.FP_STATUS_ERROR, 119 | 'data': [], 120 | 'version': mod_protocol.FP_VERSION_CURRENT 121 | }, 122 | 'check': function (output, parsed) { 123 | mod_assertplus.equal(parsed.pm_msgid, 0); 124 | mod_assertplus.equal(parsed.pm_status, mod_protocol.FP_STATUS_ERROR); 125 | } 126 | }, { 127 | 'name': 'maximum msgid', 128 | 'input': { 129 | 'msgid': 2147483647, 130 | 'status': mod_protocol.FP_STATUS_END, 131 | 'data': [ 'hello' ], 132 | 'version': mod_protocol.FP_VERSION_CURRENT 133 | }, 134 | 'check': function (output, parsed) { 135 | mod_assertplus.equal(parsed.pm_msgid, 2147483647); 136 | mod_assertplus.equal(parsed.pm_status, mod_protocol.FP_STATUS_END); 137 | mod_assertplus.equal(parsed.pm_version, 138 | mod_protocol.FP_VERSION_CURRENT); 139 | } 140 | }, { 141 | 'name': 'bad msgid: missing', 142 | 'error': /msg.msgid is not an integer between 0 and FP_MSGID_MAX/, 143 | 'input': { 144 | 'status': mod_protocol.FP_STATUS_DATA, 145 | 'data': [], 146 | 'version': mod_protocol.FP_VERSION_CURRENT 147 | } 148 | }, { 149 | 'name': 'bad msgid: negative', 150 | 'error': /msg.msgid is not an integer between 0 and FP_MSGID_MAX/, 151 | 'input': { 152 | 'msgid': -3, 153 | 'status': mod_protocol.FP_STATUS_DATA, 154 | 'data': [], 155 | 'version': mod_protocol.FP_VERSION_CURRENT 156 | } 157 | }, { 158 | 'name': 'bad msgid: too large', 159 | 'error': /msg.msgid is not an integer between 0 and FP_MSGID_MAX/, 160 | 'input': { 161 | 'msgid': 2147483648, 162 | 'status': mod_protocol.FP_STATUS_DATA, 163 | 'data': [], 164 | 'version': mod_protocol.FP_VERSION_CURRENT 165 | } 166 | }, { 167 | 'name': 'bad msgid: non-integer', 168 | 'error': /msg.msgid is not an integer between 0 and FP_MSGID_MAX/, 169 | 'input': { 170 | 'msgid': 3.7, 171 | 'status': mod_protocol.FP_STATUS_DATA, 172 | 'data': [], 173 | 'version': mod_protocol.FP_VERSION_CURRENT 174 | } 175 | }, { 176 | 'name': 'bad msgid: non-numeric', 177 | 'error': /msg.msgid is not an integer between 0 and FP_MSGID_MAX/, 178 | 'input': { 179 | 'msgid': {}, 180 | 'status': mod_protocol.FP_STATUS_DATA, 181 | 'data': [], 182 | 'version': mod_protocol.FP_VERSION_CURRENT 183 | } 184 | }, { 185 | 'name': 'bad status: missing', 186 | 'error': /msg.status \(number\) is required/, 187 | 'input': { 188 | 'msgid': 17, 189 | 'data': [], 190 | 'version': mod_protocol.FP_VERSION_CURRENT 191 | } 192 | }, { 193 | 'name': 'bad status: non-numeric', 194 | 'error': /msg.status \(number\) is required/, 195 | 'input': { 196 | 'msgid': 17, 197 | 'status': {}, 198 | 'data': [], 199 | 'version': mod_protocol.FP_VERSION_CURRENT 200 | } 201 | }, { 202 | 'name': 'bad status: unsupported value (4)', 203 | 'error': /unsupported fast message status/, 204 | 'input': { 205 | 'msgid': 17, 206 | 'status': 4, 207 | 'data': [], 208 | 'version': mod_protocol.FP_VERSION_CURRENT 209 | } 210 | }, { 211 | 'name': 'bad status: unsupported value (0)', 212 | 'error': /unsupported fast message status/, 213 | 'input': { 214 | 'msgid': 17, 215 | 'status': 0, 216 | 'data': [], 217 | 'version': mod_protocol.FP_VERSION_CURRENT 218 | } 219 | }, { 220 | 'name': 'bad data: missing', 221 | 'error': /msg.data \(object\) is required/, 222 | 'input': { 223 | 'msgid': 17, 224 | 'status': mod_protocol.FP_STATUS_ERROR, 225 | 'version': mod_protocol.FP_VERSION_CURRENT 226 | } 227 | }, { 228 | 'name': 'bad data: null', 229 | 'error': /msg.data \(object\) is required/, 230 | 'input': { 231 | 'msgid': 17, 232 | 'status': mod_protocol.FP_STATUS_ERROR, 233 | 'data': null, 234 | 'version': mod_protocol.FP_VERSION_CURRENT 235 | } 236 | }, { 237 | 'name': 'bad data: numeric', 238 | 'error': /msg.data \(object\) is required/, 239 | 'input': { 240 | 'msgid': 17, 241 | 'status': mod_protocol.FP_STATUS_ERROR, 242 | 'data': 47, 243 | 'version': mod_protocol.FP_VERSION_CURRENT 244 | } 245 | }, { 246 | 'name': 'bad data: not stringifiable', 247 | 'error': /Converting circular structure to JSON/, 248 | 'input': { 249 | 'msgid': 17, 250 | 'status': mod_protocol.FP_STATUS_ERROR, 251 | 'data': [ circular ], 252 | 'version': mod_protocol.FP_VERSION_CURRENT 253 | } 254 | } ]; 255 | 256 | function runTestCase(testcase) 257 | { 258 | var error, outbuf, parsed; 259 | 260 | printf('test case: %s: ', testcase.name); 261 | 262 | if (typeof (testcase['input']['data']) == 'function') { 263 | testcase['input']['data'] = testcase['input']['data'](); 264 | } 265 | 266 | try { 267 | outbuf = mod_protocol.fastMessageEncode(testcase.input); 268 | } catch (ex) { 269 | error = ex; 270 | } 271 | 272 | if (error !== undefined) { 273 | if (!testcase['error']) { 274 | printf('FAIL\n'); 275 | printf('expected success, found error: %s\n', 276 | error.stack); 277 | throw (error); 278 | } 279 | 280 | if (!testcase['error'].test(error.message)) { 281 | printf('FAIL\n'); 282 | printf('expected error to match: %s\n', 283 | testcase['error'].source); 284 | printf('found error: %s\n', error.stack); 285 | throw (error); 286 | } 287 | } else { 288 | if (testcase['error']) { 289 | printf('FAIL\n'); 290 | printf('expected error to match: %s\n', 291 | testcase['error'].source); 292 | printf('found success\n'); 293 | throw (new Error('test case failed')); 294 | } 295 | 296 | /* 297 | * Check conditions that should be true for all success cases. 298 | */ 299 | mod_assertplus.ok(Buffer.isBuffer(outbuf)); 300 | mod_assertplus.ok(outbuf.length > mod_protocol.FP_HEADER_SZ); 301 | mod_assertplus.equal(0x1, 302 | outbuf.readUInt8(mod_protocol.FP_OFF_TYPE)); 303 | 304 | parsed = {}; 305 | parsed.pm_datalen = 306 | outbuf.readUInt32BE(mod_protocol.FP_OFF_DATALEN); 307 | parsed.pm_data = outbuf.slice(mod_protocol.FP_OFF_DATA); 308 | parsed.pm_status = outbuf.readUInt8(mod_protocol.FP_OFF_STATUS); 309 | parsed.pm_msgid = 310 | outbuf.readUInt32BE(mod_protocol.FP_OFF_MSGID); 311 | parsed.pm_crc = outbuf.readUInt32BE(mod_protocol.FP_OFF_CRC); 312 | parsed.pm_version = 313 | outbuf.readUInt8(mod_protocol.FP_OFF_VERSION); 314 | 315 | mod_assertplus.ok(parsed.pm_status > 0 && 316 | parsed.pm_status <= 0x3); 317 | testcase['check'](outbuf, parsed); 318 | } 319 | 320 | printf('ok\n'); 321 | } 322 | 323 | main(); 324 | -------------------------------------------------------------------------------- /test/tst.socket_summarize.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | * Copyright (c) 2016, Joyent, Inc. 9 | */ 10 | 11 | /* 12 | * test/tst.socket_summarize.js: tests summarizeSocketAddrs function. 13 | */ 14 | 15 | var mod_assertplus = require('assert-plus'); 16 | var mod_fs = require('fs'); 17 | var mod_net = require('net'); 18 | var mod_path = require('path'); 19 | var mod_vasync = require('vasync'); 20 | var VError = require('verror'); 21 | 22 | var mod_subr = require('../lib/subr'); 23 | var mod_testcommon = require('./common'); 24 | 25 | var serverPort = mod_testcommon.serverPort; 26 | var serverUds = '/tmp/tst.socket_summarize.js'; 27 | 28 | function main() 29 | { 30 | mod_testcommon.registerExitBlocker('test run'); 31 | mod_vasync.forEachPipeline({ 32 | 'inputs': test_cases, 33 | 'func': runTestCase 34 | }, function (err) { 35 | if (err) { 36 | throw (err); 37 | } 38 | 39 | mod_testcommon.unregisterExitBlocker('test run'); 40 | console.log('%s tests passed', mod_path.basename(__filename)); 41 | }); 42 | } 43 | 44 | function runTestCase(testcase, callback) 45 | { 46 | var barrier, server, client; 47 | var servers_socket; 48 | 49 | console.log('test case: %s', testcase['name']); 50 | 51 | barrier = mod_vasync.barrier(); 52 | server = mod_net.createServer(); 53 | testcase['listen'](server, function (err) { 54 | mod_assertplus.ok(!err); 55 | barrier.start('client connection'); 56 | client = testcase['connect'](); 57 | client.on('connect', function () { 58 | if (testcase['cleanup']) { 59 | testcase['cleanup'](function () { 60 | barrier.done('client connection'); 61 | }); 62 | } else { 63 | barrier.done('client connection'); 64 | } 65 | }); 66 | 67 | barrier.start('server connection'); 68 | server.on('connection', function (s) { 69 | servers_socket = s; 70 | barrier.done('server connection'); 71 | }); 72 | }); 73 | 74 | barrier.on('drain', function () { 75 | var serverSummary, clientSummary; 76 | 77 | serverSummary = mod_subr.summarizeSocketAddrs(servers_socket); 78 | clientSummary = mod_subr.summarizeSocketAddrs(client); 79 | testcase['check'](serverSummary, clientSummary); 80 | server.close(); 81 | servers_socket.destroy(); 82 | client.destroy(); 83 | callback(); 84 | }); 85 | } 86 | 87 | var test_cases = [ { 88 | 'name': 'IPv4 sockets', 89 | 'listen': function (server, callback) { 90 | server.listen(serverPort, '127.0.0.1', callback); 91 | }, 92 | 'connect': function () { 93 | return (mod_net.createConnection(serverPort, '127.0.0.1')); 94 | }, 95 | 'check': function (serverSummary, clientSummary) { 96 | mod_assertplus.equal(serverSummary.localAddress, '127.0.0.1'); 97 | mod_assertplus.equal(serverSummary.localPort, serverPort); 98 | mod_assertplus.equal(serverSummary.remoteAddress, '127.0.0.1'); 99 | mod_assertplus.equal(serverSummary.remotePort, 100 | clientSummary.localPort); 101 | mod_assertplus.equal(serverSummary.socketType, 'IPv4'); 102 | mod_assertplus.equal(serverSummary.label, 103 | '127.0.0.1:' + clientSummary.localPort); 104 | 105 | mod_assertplus.equal(clientSummary.localAddress, '127.0.0.1'); 106 | mod_assertplus.equal(clientSummary.remoteAddress, '127.0.0.1'); 107 | mod_assertplus.equal(clientSummary.remotePort, serverPort); 108 | mod_assertplus.equal(clientSummary.socketType, 'IPv4'); 109 | mod_assertplus.equal(clientSummary.label, 110 | '127.0.0.1:' + serverPort); 111 | } 112 | 113 | }, { 114 | 'name': 'IPv6 sockets', 115 | 'listen': function (server, callback) { 116 | server.listen(serverPort, '::1', callback); 117 | }, 118 | 'connect': function () { 119 | return (mod_net.createConnection(serverPort, '::1')); 120 | }, 121 | 'check': function (serverSummary, clientSummary) { 122 | mod_assertplus.equal(serverSummary.localAddress, '::1'); 123 | mod_assertplus.equal(serverSummary.localPort, serverPort); 124 | mod_assertplus.equal(serverSummary.remoteAddress, '::1'); 125 | mod_assertplus.equal(serverSummary.remotePort, 126 | clientSummary.localPort); 127 | mod_assertplus.equal(serverSummary.socketType, 'IPv6'); 128 | mod_assertplus.equal(serverSummary.label, 129 | '::1:' + clientSummary.localPort); 130 | 131 | mod_assertplus.equal(clientSummary.localAddress, '::1'); 132 | mod_assertplus.equal(clientSummary.remoteAddress, '::1'); 133 | mod_assertplus.equal(clientSummary.remotePort, serverPort); 134 | mod_assertplus.equal(clientSummary.socketType, 'IPv6'); 135 | mod_assertplus.equal(clientSummary.label, '::1:' + serverPort); 136 | } 137 | 138 | }, { 139 | 'name': 'UDS sockets', 140 | 'listen': function (server, callback) { 141 | cleanupUds(function () { 142 | server.listen(serverUds, callback); 143 | }); 144 | }, 145 | 'connect': function () { 146 | return (mod_net.createConnection(serverUds)); 147 | }, 148 | 'check': function (serverSummary, clientSummary) { 149 | mod_assertplus.equal(serverSummary.socketType, 'UDS (inferred)'); 150 | mod_assertplus.equal(serverSummary.label, 'UDS'); 151 | mod_assertplus.equal(clientSummary.socketType, 'UDS (inferred)'); 152 | mod_assertplus.equal(clientSummary.label, 'UDS'); 153 | }, 154 | 'cleanup': cleanupUds 155 | } ]; 156 | 157 | function cleanupUds(callback) 158 | { 159 | mod_fs.unlink(serverUds, function (err) { 160 | if (err && err['code'] != 'ENOENT') { 161 | throw (new VError(err, 'failed to unlink "%s"', 162 | serverUds)); 163 | } 164 | 165 | callback(); 166 | }); 167 | } 168 | 169 | main(); 170 | -------------------------------------------------------------------------------- /tools/bashstyle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | * This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | */ 7 | 8 | /* 9 | * Copyright (c) 2014, Joyent, Inc. 10 | */ 11 | 12 | /* 13 | * bashstyle: check bash scripts for adherence to style guidelines, including: 14 | * 15 | * o no lines longer than 80 characters 16 | * o file does not end with a blank line 17 | * 18 | * Future enhancements could include: 19 | * o indents consistent with respect to tabs, spaces 20 | * o indents consistently sized (all are some multiple of the smallest 21 | * indent, which must be a tab or 4 or 8 spaces) 22 | */ 23 | 24 | var mod_assert = require('assert'); 25 | var mod_fs = require('fs'); 26 | 27 | var nerrors = 0; 28 | 29 | main(); 30 | process.exit(0); 31 | 32 | function main() 33 | { 34 | var files = process.argv.slice(2); 35 | 36 | if (files.length === 0) { 37 | console.error('usage: %s file1 [...]', 38 | process.argv.slice(0, 2).join(' ')); 39 | process.exit(2); 40 | } 41 | 42 | files.forEach(checkFile); 43 | 44 | if (nerrors != 0) 45 | process.exit(1); 46 | } 47 | 48 | function checkFile(filename) 49 | { 50 | var text = mod_fs.readFileSync(filename, 'utf-8'); 51 | var lines = text.split('\n'); 52 | var i; 53 | 54 | mod_assert.ok(lines.length > 0); 55 | 56 | /* 57 | * Expand tabs in each line and check for long lines. 58 | */ 59 | for (i = 1; i <= lines.length; i++) { 60 | var line = expandTabs(lines[i - 1]); 61 | 62 | if (line.length > 80) { 63 | nerrors++; 64 | console.log('%s: %d: line exceeds 80 columns', 65 | filename, i); 66 | } 67 | } 68 | 69 | /* 70 | * No sane editor lets you save a file without a newline at the very 71 | * end. 72 | */ 73 | if (lines[lines.length - 1].length !== 0) { 74 | nerrors++; 75 | console.log('%s: %d: file does not end with newline', 76 | filename, lines.length); 77 | } 78 | 79 | /* 80 | * Since the file will always end with a newline, the last entry of 81 | * "lines" will actually be blank. 82 | */ 83 | if (lines.length > 1 && lines[lines.length - 2].length === 0) { 84 | nerrors++; 85 | console.log('%s: %d: file ends with a blank line', 86 | filename, lines.length - 1); 87 | } 88 | } 89 | 90 | function expandTabs(text) 91 | { 92 | var out = ''; 93 | var col = 0; 94 | var j, k; 95 | 96 | for (j = 0; j < text.length; j++) { 97 | if (text[j] != '\t') { 98 | out += text[j]; 99 | col++; 100 | continue; 101 | } 102 | 103 | k = 8 - (col % 8); 104 | col += k; 105 | 106 | do { 107 | out += ' '; 108 | } while (--k > 0); 109 | 110 | col += k; 111 | } 112 | 113 | return (out); 114 | } 115 | -------------------------------------------------------------------------------- /tools/jsl.node.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration File for JavaScript Lint 3 | # 4 | # This configuration file can be used to lint a collection of scripts, or to enable 5 | # or disable warnings for scripts that are linted via the command line. 6 | # 7 | 8 | ### Warnings 9 | # Enable or disable warnings based on requirements. 10 | # Use "+WarningName" to display or "-WarningName" to suppress. 11 | # 12 | +ambiguous_else_stmt # the else statement could be matched with one of multiple if statements (use curly braces to indicate intent 13 | +ambiguous_nested_stmt # block statements containing block statements should use curly braces to resolve ambiguity 14 | +ambiguous_newline # unexpected end of line; it is ambiguous whether these lines are part of the same statement 15 | +anon_no_return_value # anonymous function does not always return value 16 | +assign_to_function_call # assignment to a function call 17 | -block_without_braces # block statement without curly braces 18 | +comma_separated_stmts # multiple statements separated by commas (use semicolons?) 19 | +comparison_type_conv # comparisons against null, 0, true, false, or an empty string allowing implicit type conversion (use === or !==) 20 | +default_not_at_end # the default case is not at the end of the switch statement 21 | +dup_option_explicit # duplicate "option explicit" control comment 22 | +duplicate_case_in_switch # duplicate case in switch statement 23 | +duplicate_formal # duplicate formal argument {name} 24 | +empty_statement # empty statement or extra semicolon 25 | +identifier_hides_another # identifer {name} hides an identifier in a parent scope 26 | -inc_dec_within_stmt # increment (++) and decrement (--) operators used as part of greater statement 27 | +incorrect_version # Expected /*jsl:content-type*/ control comment. The script was parsed with the wrong version. 28 | +invalid_fallthru # unexpected "fallthru" control comment 29 | +invalid_pass # unexpected "pass" control comment 30 | +jsl_cc_not_understood # couldn't understand control comment using /*jsl:keyword*/ syntax 31 | +leading_decimal_point # leading decimal point may indicate a number or an object member 32 | +legacy_cc_not_understood # couldn't understand control comment using /*@keyword@*/ syntax 33 | +meaningless_block # meaningless block; curly braces have no impact 34 | +mismatch_ctrl_comments # mismatched control comment; "ignore" and "end" control comments must have a one-to-one correspondence 35 | +misplaced_regex # regular expressions should be preceded by a left parenthesis, assignment, colon, or comma 36 | +missing_break # missing break statement 37 | +missing_break_for_last_case # missing break statement for last case in switch 38 | +missing_default_case # missing default case in switch statement 39 | +missing_option_explicit # the "option explicit" control comment is missing 40 | +missing_semicolon # missing semicolon 41 | +missing_semicolon_for_lambda # missing semicolon for lambda assignment 42 | +multiple_plus_minus # unknown order of operations for successive plus (e.g. x+++y) or minus (e.g. x---y) signs 43 | +nested_comment # nested comment 44 | +no_return_value # function {name} does not always return a value 45 | +octal_number # leading zeros make an octal number 46 | +parseint_missing_radix # parseInt missing radix parameter 47 | +partial_option_explicit # the "option explicit" control comment, if used, must be in the first script tag 48 | +redeclared_var # redeclaration of {name} 49 | +trailing_comma_in_array # extra comma is not recommended in array initializers 50 | +trailing_decimal_point # trailing decimal point may indicate a number or an object member 51 | +undeclared_identifier # undeclared identifier: {name} 52 | +unreachable_code # unreachable code 53 | -unreferenced_argument # argument declared but never referenced: {name} 54 | -unreferenced_function # function is declared but never referenced: {name} 55 | +unreferenced_variable # variable is declared but never referenced: {name} 56 | +unsupported_version # JavaScript {version} is not supported 57 | +use_of_label # use of label 58 | +useless_assign # useless assignment 59 | +useless_comparison # useless comparison; comparing identical expressions 60 | -useless_quotes # the quotation marks are unnecessary 61 | +useless_void # use of the void type may be unnecessary (void is always undefined) 62 | +var_hides_arg # variable {name} hides argument 63 | +want_assign_or_call # expected an assignment or function call 64 | +with_statement # with statement hides undeclared variables; use temporary variable instead 65 | 66 | 67 | ### Output format 68 | # Customize the format of the error message. 69 | # __FILE__ indicates current file path 70 | # __FILENAME__ indicates current file name 71 | # __LINE__ indicates current line 72 | # __COL__ indicates current column 73 | # __ERROR__ indicates error message (__ERROR_PREFIX__: __ERROR_MSG__) 74 | # __ERROR_NAME__ indicates error name (used in configuration file) 75 | # __ERROR_PREFIX__ indicates error prefix 76 | # __ERROR_MSG__ indicates error message 77 | # 78 | # For machine-friendly output, the output format can be prefixed with 79 | # "encode:". If specified, all items will be encoded with C-slashes. 80 | # 81 | # Visual Studio syntax (default): 82 | +output-format __FILE__(__LINE__): __ERROR__ 83 | # Alternative syntax: 84 | #+output-format __FILE__:__LINE__: __ERROR__ 85 | 86 | 87 | ### Context 88 | # Show the in-line position of the error. 89 | # Use "+context" to display or "-context" to suppress. 90 | # 91 | +context 92 | 93 | 94 | ### Control Comments 95 | # Both JavaScript Lint and the JScript interpreter confuse each other with the syntax for 96 | # the /*@keyword@*/ control comments and JScript conditional comments. (The latter is 97 | # enabled in JScript with @cc_on@). The /*jsl:keyword*/ syntax is preferred for this reason, 98 | # although legacy control comments are enabled by default for backward compatibility. 99 | # 100 | -legacy_control_comments 101 | 102 | 103 | ### Defining identifiers 104 | # By default, "option explicit" is enabled on a per-file basis. 105 | # To enable this for all files, use "+always_use_option_explicit" 106 | -always_use_option_explicit 107 | 108 | # Define certain identifiers of which the lint is not aware. 109 | # (Use this in conjunction with the "undeclared identifier" warning.) 110 | # 111 | # Common uses for webpages might be: 112 | +define __dirname 113 | +define clearInterval 114 | +define clearTimeout 115 | +define console 116 | +define exports 117 | +define global 118 | +define module 119 | +define process 120 | +define require 121 | +define setImmediate 122 | +define clearImmediate 123 | +define setInterval 124 | +define setTimeout 125 | +define Buffer 126 | +define JSON 127 | +define Math 128 | +define __dirname 129 | +define __filename 130 | 131 | ### JavaScript Version 132 | # To change the default JavaScript version: 133 | #+default-type text/javascript;version=1.5 134 | #+default-type text/javascript;e4x=1 135 | 136 | ### Files 137 | # Specify which files to lint 138 | # Use "+recurse" to enable recursion (disabled by default). 139 | # To add a set of files, use "+process FileName", "+process Folder\Path\*.js", 140 | # or "+process Folder\Path\*.htm". 141 | # 142 | 143 | -------------------------------------------------------------------------------- /tools/mk/Makefile.deps: -------------------------------------------------------------------------------- 1 | # -*- mode: makefile -*- 2 | # 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | # 7 | 8 | # 9 | # Copyright (c) 2014, Joyent, Inc. 10 | # 11 | 12 | # 13 | # Makefile.deps: Makefile for including common tools as dependencies 14 | # 15 | # NOTE: This makefile comes from the "eng" repo. It's designed to be dropped 16 | # into other repos as-is without requiring any modifications. If you find 17 | # yourself changing this file, you should instead update the original copy in 18 | # eng.git and then update your repo to use the new version. 19 | # 20 | # This file is separate from Makefile.targ so that teams can choose 21 | # independently whether to use the common targets in Makefile.targ and the 22 | # common tools here. 23 | # 24 | 25 | # 26 | # javascriptlint 27 | # 28 | JSL_EXEC ?= deps/javascriptlint/build/install/jsl 29 | JSL ?= $(JSL_EXEC) 30 | 31 | $(JSL_EXEC): | deps/javascriptlint/.git 32 | cd deps/javascriptlint && make install 33 | 34 | distclean:: 35 | if [[ -f deps/javascriptlint/Makefile ]]; then \ 36 | cd deps/javascriptlint && make clean; \ 37 | fi 38 | 39 | # 40 | # jsstyle 41 | # 42 | JSSTYLE_EXEC ?= deps/jsstyle/jsstyle 43 | JSSTYLE ?= $(JSSTYLE_EXEC) 44 | 45 | $(JSSTYLE_EXEC): | deps/jsstyle/.git 46 | 47 | # 48 | # restdown 49 | # 50 | RESTDOWN_EXEC ?= deps/restdown/bin/restdown 51 | RESTDOWN ?= python $(RESTDOWN_EXEC) 52 | $(RESTDOWN_EXEC): | deps/restdown/.git 53 | 54 | EXTRA_DOC_DEPS ?= 55 | -------------------------------------------------------------------------------- /tools/mk/Makefile.targ: -------------------------------------------------------------------------------- 1 | # 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | # 6 | 7 | # 8 | # Copyright (c) 2017, Joyent, Inc. 9 | # 10 | 11 | # 12 | # Makefile.targ: common targets. 13 | # 14 | # NOTE: This makefile comes from the "eng" repo. It's designed to be dropped 15 | # into other repos as-is without requiring any modifications. If you find 16 | # yourself changing this file, you should instead update the original copy in 17 | # eng.git and then update your repo to use the new version. 18 | # 19 | # This Makefile defines several useful targets and rules. You can use it by 20 | # including it from a Makefile that specifies some of the variables below. 21 | # 22 | # Targets defined in this Makefile: 23 | # 24 | # check Checks JavaScript files for lint and style 25 | # Checks bash scripts for syntax 26 | # Checks SMF manifests for validity against the SMF DTD 27 | # 28 | # clean Removes built files 29 | # 30 | # docs Builds restdown documentation in docs/ 31 | # 32 | # prepush Depends on "check" and "test" 33 | # 34 | # test Does nothing (you should override this) 35 | # 36 | # xref Generates cscope (source cross-reference index) 37 | # 38 | # For details on what these targets are supposed to do, see the Joyent 39 | # Engineering Guide. 40 | # 41 | # To make use of these targets, you'll need to set some of these variables. Any 42 | # variables left unset will simply not be used. 43 | # 44 | # BASH_FILES Bash scripts to check for syntax 45 | # (paths relative to top-level Makefile) 46 | # 47 | # CLEAN_FILES Files to remove as part of the "clean" target. Note 48 | # that files generated by targets in this Makefile are 49 | # automatically included in CLEAN_FILES. These include 50 | # restdown-generated HTML and JSON files. 51 | # 52 | # DOC_FILES Restdown (documentation source) files. These are 53 | # assumed to be contained in "docs/", and must NOT 54 | # contain the "docs/" prefix. 55 | # 56 | # JSL_CONF_NODE Specify JavaScriptLint configuration files 57 | # JSL_CONF_WEB (paths relative to top-level Makefile) 58 | # 59 | # Node.js and Web configuration files are separate 60 | # because you'll usually want different global variable 61 | # configurations. If no file is specified, none is given 62 | # to jsl, which causes it to use a default configuration, 63 | # which probably isn't what you want. 64 | # 65 | # JSL_FILES_NODE JavaScript files to check with Node config file. 66 | # JSL_FILES_WEB JavaScript files to check with Web config file. 67 | # 68 | # JSON_FILES JSON files to be validated 69 | # 70 | # JSSTYLE_FILES JavaScript files to be style-checked 71 | # 72 | # You can also override these variables: 73 | # 74 | # BASH Path to bash (default: "bash") 75 | # 76 | # CSCOPE_DIRS Directories to search for source files for the cscope 77 | # index. (default: ".") 78 | # 79 | # JSL Path to JavaScriptLint (default: "jsl") 80 | # 81 | # JSL_FLAGS_NODE Additional flags to pass through to JSL 82 | # JSL_FLAGS_WEB 83 | # JSL_FLAGS 84 | # 85 | # JSON Path to json tool (default: "json") 86 | # 87 | # JSSTYLE Path to jsstyle (default: "jsstyle") 88 | # 89 | # JSSTYLE_FLAGS Additional flags to pass through to jsstyle 90 | # 91 | # RESTDOWN_EXT By default '.md' is required for DOC_FILES (see above). 92 | # If you want to use, say, '.restdown' instead, then set 93 | # 'RESTDOWN_EXT=.restdown' in your Makefile. 94 | # 95 | 96 | # 97 | # Defaults for the various tools we use. 98 | # 99 | BASH ?= bash 100 | BASHSTYLE ?= tools/bashstyle 101 | CP ?= cp 102 | CSCOPE ?= cscope 103 | CSCOPE_DIRS ?= . 104 | JSL ?= jsl 105 | JSON ?= json 106 | JSSTYLE ?= jsstyle 107 | MKDIR ?= mkdir -p 108 | MV ?= mv 109 | RESTDOWN_FLAGS ?= 110 | RESTDOWN_EXT ?= .md 111 | RMTREE ?= rm -rf 112 | JSL_FLAGS ?= --nologo --nosummary 113 | 114 | ifeq ($(shell uname -s),SunOS) 115 | TAR ?= gtar 116 | else 117 | TAR ?= tar 118 | endif 119 | 120 | 121 | # 122 | # Defaults for other fixed values. 123 | # 124 | BUILD = build 125 | DISTCLEAN_FILES += $(BUILD) 126 | DOC_BUILD = $(BUILD)/docs/public 127 | 128 | # 129 | # Configure JSL_FLAGS_{NODE,WEB} based on JSL_CONF_{NODE,WEB}. 130 | # 131 | ifneq ($(origin JSL_CONF_NODE), undefined) 132 | JSL_FLAGS_NODE += --conf=$(JSL_CONF_NODE) 133 | endif 134 | 135 | ifneq ($(origin JSL_CONF_WEB), undefined) 136 | JSL_FLAGS_WEB += --conf=$(JSL_CONF_WEB) 137 | endif 138 | 139 | # 140 | # Targets. For descriptions on what these are supposed to do, see the 141 | # Joyent Engineering Guide. 142 | # 143 | 144 | # 145 | # Instruct make to keep around temporary files. We have rules below that 146 | # automatically update git submodules as needed, but they employ a deps/*/.git 147 | # temporary file. Without this directive, make tries to remove these .git 148 | # directories after the build has completed. 149 | # 150 | .SECONDARY: $($(wildcard deps/*):%=%/.git) 151 | 152 | # 153 | # This rule enables other rules that use files from a git submodule to have 154 | # those files depend on deps/module/.git and have "make" automatically check 155 | # out the submodule as needed. 156 | # 157 | deps/%/.git: 158 | git submodule update --init deps/$* 159 | 160 | # 161 | # These recipes make heavy use of dynamically-created phony targets. The parent 162 | # Makefile defines a list of input files like BASH_FILES. We then say that each 163 | # of these files depends on a fake target called filename.bashchk, and then we 164 | # define a pattern rule for those targets that runs bash in check-syntax-only 165 | # mode. This mechanism has the nice properties that if you specify zero files, 166 | # the rule becomes a noop (unlike a single rule to check all bash files, which 167 | # would invoke bash with zero files), and you can check individual files from 168 | # the command line with "make filename.bashchk". 169 | # 170 | .PHONY: check-bash 171 | check-bash: $(BASH_FILES:%=%.bashchk) $(BASH_FILES:%=%.bashstyle) 172 | 173 | %.bashchk: % 174 | $(BASH) -n $^ 175 | 176 | %.bashstyle: % 177 | $(BASHSTYLE) $^ 178 | 179 | .PHONY: check-json 180 | check-json: $(JSON_FILES:%=%.jsonchk) 181 | 182 | %.jsonchk: % 183 | $(JSON) --validate -f $^ 184 | 185 | # 186 | # The above approach can be slow when there are many files to check because it 187 | # requires that "make" invoke the check tool once for each file, rather than 188 | # passing in several files at once. For the JavaScript check targets, we define 189 | # a variable for the target itself *only if* the list of input files is 190 | # non-empty. This avoids invoking the tool if there are no files to check. 191 | # 192 | JSL_NODE_TARGET = $(if $(JSL_FILES_NODE), check-jsl-node) 193 | .PHONY: check-jsl-node 194 | check-jsl-node: $(JSL_EXEC) 195 | $(JSL) $(JSL_FLAGS) $(JSL_FLAGS_NODE) $(JSL_FILES_NODE) 196 | 197 | JSL_WEB_TARGET = $(if $(JSL_FILES_WEB), check-jsl-web) 198 | .PHONY: check-jsl-web 199 | check-jsl-web: $(JSL_EXEC) 200 | $(JSL) $(JSL_FLAGS) $(JSL_FLAGS_WEB) $(JSL_FILES_WEB) 201 | 202 | .PHONY: check-jsl 203 | check-jsl: $(JSL_NODE_TARGET) $(JSL_WEB_TARGET) 204 | 205 | JSSTYLE_TARGET = $(if $(JSSTYLE_FILES), check-jsstyle) 206 | .PHONY: check-jsstyle 207 | check-jsstyle: $(JSSTYLE_EXEC) 208 | $(JSSTYLE) $(JSSTYLE_FLAGS) $(JSSTYLE_FILES) 209 | 210 | .PHONY: check 211 | check:: check-jsl check-json $(JSSTYLE_TARGET) check-bash 212 | @echo check ok 213 | 214 | .PHONY: clean 215 | clean:: 216 | -$(RMTREE) $(CLEAN_FILES) 217 | 218 | .PHONY: distclean 219 | distclean:: clean 220 | -$(RMTREE) $(DISTCLEAN_FILES) 221 | 222 | CSCOPE_FILES = cscope.in.out cscope.out cscope.po.out 223 | CLEAN_FILES += $(CSCOPE_FILES) 224 | 225 | .PHONY: xref 226 | xref: cscope.files 227 | $(CSCOPE) -bqR 228 | 229 | .PHONY: cscope.files 230 | cscope.files: 231 | find $(CSCOPE_DIRS) -name '*.c' -o -name '*.h' -o -name '*.cc' \ 232 | -o -name '*.js' -o -name '*.s' -o -name '*.cpp' > $@ 233 | 234 | # 235 | # The "docs" target is complicated because we do several things here: 236 | # 237 | # (1) Use restdown to build HTML and JSON files from each of DOC_FILES. 238 | # 239 | # (2) Copy these files into $(DOC_BUILD) (build/docs/public), which 240 | # functions as a complete copy of the documentation that could be 241 | # mirrored or served over HTTP. 242 | # 243 | # (3) Then copy any directories and media from docs/media into 244 | # $(DOC_BUILD)/media. This allows projects to include their own media, 245 | # including files that will override same-named files provided by 246 | # restdown. 247 | # 248 | # Step (3) is the surprisingly complex part: in order to do this, we need to 249 | # identify the subdirectories in docs/media, recreate them in 250 | # $(DOC_BUILD)/media, then do the same with the files. 251 | # 252 | DOC_MEDIA_DIRS := $(shell find docs/media -type d 2>/dev/null | grep -v "^docs/media$$") 253 | DOC_MEDIA_DIRS := $(DOC_MEDIA_DIRS:docs/media/%=%) 254 | DOC_MEDIA_DIRS_BUILD := $(DOC_MEDIA_DIRS:%=$(DOC_BUILD)/media/%) 255 | 256 | DOC_MEDIA_FILES := $(shell find docs/media -type f 2>/dev/null) 257 | DOC_MEDIA_FILES := $(DOC_MEDIA_FILES:docs/media/%=%) 258 | DOC_MEDIA_FILES_BUILD := $(DOC_MEDIA_FILES:%=$(DOC_BUILD)/media/%) 259 | 260 | # 261 | # Like the other targets, "docs" just depends on the final files we want to 262 | # create in $(DOC_BUILD), leveraging other targets and recipes to define how 263 | # to get there. 264 | # 265 | .PHONY: docs 266 | docs:: \ 267 | $(DOC_FILES:%$(RESTDOWN_EXT)=$(DOC_BUILD)/%.html) \ 268 | $(DOC_FILES:%$(RESTDOWN_EXT)=$(DOC_BUILD)/%.json) \ 269 | $(DOC_MEDIA_FILES_BUILD) 270 | 271 | # 272 | # We keep the intermediate files so that the next build can see whether the 273 | # files in DOC_BUILD are up to date. 274 | # 275 | .PRECIOUS: \ 276 | $(DOC_FILES:%$(RESTDOWN_EXT)=docs/%.html) \ 277 | $(DOC_FILES:%$(RESTDOWN_EXT)=docs/%json) 278 | 279 | # 280 | # We do clean those intermediate files, as well as all of DOC_BUILD. 281 | # 282 | CLEAN_FILES += \ 283 | $(DOC_BUILD) \ 284 | $(DOC_FILES:%$(RESTDOWN_EXT)=docs/%.html) \ 285 | $(DOC_FILES:%$(RESTDOWN_EXT)=docs/%.json) 286 | 287 | # 288 | # Before installing the files, we must make sure the directories exist. The | 289 | # syntax tells make that the dependency need only exist, not be up to date. 290 | # Otherwise, it might try to rebuild spuriously because the directory itself 291 | # appears out of date. 292 | # 293 | $(DOC_MEDIA_FILES_BUILD): | $(DOC_MEDIA_DIRS_BUILD) 294 | 295 | $(DOC_BUILD)/%: docs/% | $(DOC_BUILD) 296 | $(MKDIR) $(shell dirname $@) 297 | $(CP) $< $@ 298 | 299 | docs/%.json docs/%.html: docs/%$(RESTDOWN_EXT) | $(DOC_BUILD) $(RESTDOWN_EXEC) \ 300 | $(EXTRA_DOC_DEPS) 301 | $(RESTDOWN) $(RESTDOWN_FLAGS) -m $(DOC_BUILD) $< 302 | 303 | $(DOC_BUILD): 304 | $(MKDIR) $@ 305 | 306 | $(DOC_MEDIA_DIRS_BUILD): 307 | $(MKDIR) $@ 308 | 309 | # 310 | # The default "test" target does nothing. This should usually be overridden by 311 | # the parent Makefile. It's included here so we can define "prepush" without 312 | # requiring the repo to define "test". 313 | # 314 | .PHONY: test 315 | test: 316 | 317 | .PHONY: prepush 318 | prepush: check test 319 | 320 | # 321 | # This rule automatically exposes every "stamp" file as a target that can be 322 | # invoked manually as "stamp-$STAMP_NAME". For example, if a stamp has been 323 | # defined thus: 324 | # 325 | # STAMP_EXPENSIVE_RESULT := $(MAKE_STAMPS_DIR)/expensive-result 326 | # 327 | # ... this can be invoked manually as "make stamp-expensive-result". Note that 328 | # these phony targets are essentially just for interactive usage. Targets 329 | # should be specified to depend on the macro containing the stamp file name. 330 | # 331 | # See also the comments in "Makefile.defs". 332 | # 333 | stamp-%: $(MAKE_STAMPS_DIR)/% 334 | @: 335 | -------------------------------------------------------------------------------- /tools/perfrun: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # perfrun: quick-and-dirty tool to run a set of performance tests using fastbench 5 | # 6 | 7 | set -o pipefail 8 | 9 | arg0=perfrun 10 | 11 | function fail 12 | { 13 | echo "$arg0: $@" >&2 14 | exit 1 15 | } 16 | 17 | function usage 18 | { 19 | echo "$arg0 SERVER_PID SERVER_PORT TEST_NAME" >&2 20 | echo " SERVER_PID is the pid of the server." >&2 21 | echo " SERVER_PORT is the TCP port of the server to test." >&2 22 | echo " TEST_NAME is the basename of the output directory." >&2 23 | exit 2 24 | } 25 | 26 | pr_pid= 27 | pr_port= 28 | pr_testdir= 29 | pr_children= 30 | # seconds between memory samples 31 | pr_sample_mem=5 32 | # seconds between CPU usage samples 33 | pr_sample_cpu=5 34 | # seconds to wait between runs 35 | pr_wait=30 36 | pr_warmup=true 37 | pr_concurrencies="8 16 32 64 128 256 512 1024" 38 | pr_testduration=60 39 | pr_bindir= 40 | pr_fastbench= 41 | 42 | function main 43 | { 44 | local first 45 | 46 | if [[ $# != 3 ]]; then 47 | usage 48 | fi 49 | 50 | pr_pid="$1" 51 | pr_port="$2" 52 | pr_testdir="$3" 53 | if [[ -e "$pr_testdir" ]]; then 54 | fail "refusing to clobber data in \"$pr_testdir\"" 55 | fi 56 | 57 | pr_bindir="$(dirname ${BASH_SOURCE[0]})/../bin" 58 | pr_fastbench="$pr_bindir/fastbench" 59 | 60 | mkdir -p "$pr_testdir" 61 | 62 | echo "Server: pid $pr_pid on port $pr_port." 63 | echo "Will test concurrencies: $pr_concurrencies." 64 | echo "Using fastbench: $pr_fastbench" 65 | echo "Beginning data collection." 66 | trap cleanup EXIT 67 | 68 | # Start monitoring memory 69 | (while :; do 70 | echo "$(date +%s) $(ps -opid= -orss= -ovsz= -p "$pr_pid")" 71 | sleep $pr_sample_mem; 72 | done) > $pr_testdir/memory.out & 73 | pr_children="$pr_children $!" 74 | 75 | # Start monitoring CPU usage 76 | prstat -d u -Lmc -p "$pr_pid" $pr_sample_cpu > $pr_testdir/prstat.out & 77 | pr_children="$pr_children $!" 78 | echo "Child processes: $pr_children" 79 | 80 | # Wait for a few data points 81 | echo "Waiting for initial data points." 82 | sleep $pr_sample_mem 83 | sleep $pr_sample_cpu 84 | 85 | if [[ "$pr_warmup" != "false" ]]; then 86 | echo "Beginning warmup." 87 | if ! $pr_fastbench -c 1 -i 5 -d 300 sync 127.0.0.1 "$pr_port" | \ 88 | tee $pr_testdir/fastbench-warmup.out; then 89 | fail "bailing out because warmup failed" 90 | fi 91 | 92 | sleep $pr_wait 93 | fi 94 | 95 | first=true 96 | for concur in $pr_concurrencies; do 97 | if [[ $first != "true" ]]; then 98 | echo "Pausing for $pr_wait seconds." 99 | sleep $pr_wait 100 | else 101 | first=false 102 | fi 103 | 104 | echo "Starting test: concurrency = $concur" 105 | $pr_bindir/fastclatency > \ 106 | $pr_testdir/latency-sleep150-$concur.out & 107 | pr_dtrace="$!" 108 | 109 | if ! $pr_fastbench -c $concur -i 5 \ 110 | -d $pr_testduration sleep150 127.0.0.1 "$pr_port" | \ 111 | tee $pr_testdir/fastbench-sleep150-$concur.out; then 112 | kill $pr_dtrace 113 | fail "bailing out after failed test" 114 | fi 115 | 116 | kill $pr_dtrace 117 | done 118 | } 119 | 120 | function cleanup 121 | { 122 | echo "Cleaning up processes: $pr_children" 123 | for c in $pr_children; do 124 | kill $c 125 | done 126 | wait 127 | } 128 | 129 | main "$@" 130 | --------------------------------------------------------------------------------