Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

added some samples, updated readme

  • Loading branch information...
commit 103c41f8cc5ab94e052d8de2e58f2f77a18d499e 1 parent a17a906
Elad Ben-Israel authored
View
2  .gitignore
@@ -1,3 +1,3 @@
node_modules
npm-debug.log
-nploy.pids
+.DS_Store
View
2  .npmignore
@@ -1,3 +1,3 @@
node_modules
npm-debug.log
-nploy.pids
+.DS_Store
View
7 LICENSE
@@ -0,0 +1,7 @@
+Copyright (c) 2011 Microsoft Corporation
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View
145 README.md
@@ -1,105 +1,116 @@
-# Spinner
+# spinner #
-[![Build Status](https://secure.travis-ci.org/anodejs/node-spinner.png)](http://travis-ci.org/anodejs/node-spinner)
-
-Spin up node scripts bound to dynamic ports (forked from [nploy](https://github.com/stagas/nploy))
+Spawns child processes with dynamic port allocation and other goodies.
```bash
$ npm install spinner
```
-## createRouter(opts) ###
-
-Options:
+## Sample ##
- * __range__ - Port range [7000..7099]
- * __time__ - Time to idle [15]
- * __debug__ - Output logs
- * __routes__ - Hash of routing pairs (source -> target) [{}]
- * __output__ - Determines how child process output is handled:
- * __false__ - Will not capture child process output
- * __"console"__ - Will pipe child process stdin/stderr to console.info/console.error
- * __"process"__ - Will pipe child process stdin/stderr to process.stdin/process.stderr
+``js
+// basic.js
+var request = require('request');
+var spinner = require('../main').createSpinner();
-Example:
+// spawn myapp.js allocating a port in process.env.PORT
+spinner.start('myapp', function(err, port) {
+ if (err) return console.error(err);
-```js
-var spinner = require('spinner');
-var router = spinner.createRouter({ port: 5000, dir: '../test' });
-
-router.setRoutes({
- 'foo': 'a/index.js'
-, 'goo': 'b/index.js'
-});
+ // send a request to the app
+ request('http://localhost:' + port, function(err, res, body) {
+ console.log('response:', body);
-router.getRoute('foo', function(err, route, child) {
- if (err) throw new Error(err);
- console.log('Use %s:%d to access "foo"', route.host, route.port);
- router.kill('foo', function(err) {
- console.log('"foo" is now dead');
- router.close();
- });
+ // stop all child processes
+ spinner.stopall();
+ });
});
-```
-
-The `router` object has the following API.
-
+``
-### Properties ###
+Output:
- * __range__ - Returns the port range configured in the router
- * __idletime__ - Time in seconds to wait without a call to ```getRoute``` before the process is killed
- * __options__ - Options object
-
-
-### setRoute(source, target), setRoutes(map) ###
-
-`target` may be a path to a node.js script or an object with a `script` property (path to the script)
-and extra options passed to the [forever](http://github.com/nodejitsu/forever) module when starting
-the child process.
+```bash
+$ node basic.js
+[myapp] starting myapp
+[myapp] looking for an available port in the range: [ 7000, 7999 ]
+[myapp] found port 7000
+[myapp] spawn /usr/local/bin/node [ 'myapp' ]
+[myapp] waiting for port 7000 to be bound
+[myapp] checking status of port 7000 tries left: 10
+[myapp] status is closed
+[myapp] checking status of port 7000 tries left: 9
+[myapp] status is open
+[myapp] port 7000 opened successfuly
+[myapp] clearing wait timeout
+response: this is a sample app
+[myapp] stopping
+[myapp] exited with status 1
+[myapp] cleaning up
+```
-Update routes table with source -> script pair(s).
+## API ##
+### createSpinner() ##
-### getRoute(source, callback) ###
+Returns a spinner. Within a spinner namespace, child processes are identified by name and only
+a single child can exists for every name.
-Returns a route to a source. Callback is ```function(err, port, child)``` where ```port```
-is the same port passed to the app in ```process.env.PORT```.
+This means that if I call `spinner.start('foo')` twice, only a single child will be spawned. The second call will return the same port.
+### spinner.start(options, callback) ###
-### clearRoute(source), clearRoutes([map]) ###
+`options` include:
-Deletes route(s). If ```map``` is if not provided, all routes will be deleted
+ * __name__ - Name of child. Basically a key used to identify the child process
+ * __command__ - Program to execute (default is `process.execPath`, which is node.js)
+ * __args__ - Array of arguments to use for spawn
+ * __logger__ - Logger to use (default is `console`)
+ * __timeout__ - Timeout in seconds waiting for the process to bind to the allocated port
+ (default is 5 seconds).
+ * __attempts__ - Number of attempts to start the process. After this, spinner will not
+ fail on every `start` request unless a `stop` is issued.
+ * __stopTimeout___ - Timeout in seconds to wait for a child to stop before issuing a SIGKILL.
+`callback` is `function(err, port)` where `port` is the port number allocated for this child
+process and set in it's `PORT` environment variable (in node.js: `process.env.PORT`). If the child could not be started or if it did not bind to the port in the alloted `timeout`, `err` will indicate that with an `Error` object.
-### kill(source, callback) ###
+### spinner.start(script, callback) ###
-Kills the process associated with ```source```. ```callback``` is ```function(err)```.
+A short form for `spinner.start()` where `script` is used as the first argument to the node engine prescribed in `process.execPath` and also used as the name of the child.
-### close() ###
+### spinner.stop(name, callback) ###
-Shuts down the router. Namely, removes the idle timer.
+Stops the child keyed `name`. `callback` is `function(err)`.
+Spinner sends `SIGTERM` and after `stopTimeout` passes, sends `SIGKILL`.
+### spinner.stopall(callback) ###
-### getchild(source), getpid(source) ###
+Stops all the child processes maintained by this spinner.
-Returns the `forever` child of a source or it's PID.
+`callback` is `function(err)`
+### spinner.get(name) ###
-## Testing ##
+Returns information about a child process named `name`. The information includes the options
+used to start the child process and a `state` property indicating the current state of the
+child.
-Run tests:
+Possible states are:
-```bash
-npm test
-```
+ * __stopped__ - Child is stopped.
+ * __starting__ - Child is being spawned and waiting for port to be bound to.
+ * __started__ - Child is started.
+ * __stopping__ - Child is being stopped.
+ * __faulted__ - Child is faulted. That is, the alloted number of start requests failed.
+ * __restart__ - Child is being restarted.
+### spinner.list() ###
-## Licence ##
+Returns the list of child processes maintained by this spinner. The result is a hash
+keyed by the child name and contains the details from `spinner.get()`.
-MIT/X11
-__Author (nploy)__: George Stagas (@stagas)
+## License ##
-__Author (spinner)__: Elad Ben-Israel (@eladb)
+MIT
View
401 lib/spinner.js
@@ -0,0 +1,401 @@
+var spawn = require('child_process').spawn;
+var fsmjs = require('fsmjs');
+var async = require('async');
+var portscanner = require('portscanner');
+var ctxobj = require('ctxobj');
+
+exports.createSpinner = function() {
+ var api = {};
+
+ api.start = function(options, callback) {
+ if (!callback) callback = function() {};
+ if (!options) throw new Error("first argument should be a options hash or a script path");
+
+ // if options is a string, treat as script
+ if (typeof options === "string") options = { script: options };
+
+ // if script is provided, use it as the first argument
+ if (options.script) {
+ options.name = options.name || options.script;
+ if (options.args) options.args.unshift(options.script);
+ else options.args = [ options.script ];
+ delete options.script;
+ }
+
+ // if command is not provided, default to node.js
+ if (!options.command) options.command = process.execPath;
+
+ // logger can be overriden
+ var logger = options.logger || console;
+ delete options.logger;
+ logger = ctxobj.console(logger).pushctx(options.name);
+
+ // default wait timeout is 5 seconds
+ options.timeout = options.timeout || 5;
+
+ // default number of start attempts before staying in 'faulted' is 3
+ options.attempts = options.attempts || 3;
+
+ // stop timeout defaults to 30 sec
+ options.stopTimeout = options.stopTimeout || 30;
+
+ // make sure we have a name
+ if (!options.name) throw new Error('options.name is required');
+
+ // obtain a spinner obj
+ var fsm = spinner(options.name);
+
+ // store options & logger
+ fsm.options = options;
+ fsm.logger = logger;
+ fsm.name = options.name;
+
+ // sign for success/failure events
+ fsm.once('started', function() { return callback(null, fsm.port); });
+ fsm.once('error', function() { return callback(new Error("unable to start")); });
+ fsm.setMaxListeners(1000);
+
+ // hit it
+ fsm.trigger('start');
+ return fsm;
+ };
+
+ api.stop = function(script, callback) {
+ if (!callback) callback = function() {};
+ var fsm = spinner(script);
+
+ fsm.once('stopped', function(status) {
+ return callback(null, status);
+ });
+
+ fsm.once('error', function(e) {
+ return callback(e);
+ });
+
+ fsm.trigger('stop');
+ return fsm;
+ };
+
+ api.stopall = function(callback) {
+ if (!callback) callback = function() {};
+ return async.forEach(
+ Object.keys(spinnerByName),
+ function(name, cb) { api.stop(name, cb); },
+ callback);
+ };
+
+ api.list = function() {
+ var result = {};
+ for (var name in spinnerByName) {
+ result[name] = api.get(name);
+ }
+ return result;
+ };
+
+ api.get = function(name) {
+ var fsm = spinnerByName[name];
+ if (!fsm) return null;
+
+ var desc = fsm.options;
+
+ switch (fsm.state) {
+ case 'start':
+ case 'wait':
+ desc.state = 'starting';
+ break;
+
+ case 'stop':
+ desc.state = 'stopping';
+ break;
+
+ case 'restart':
+ desc.state = 'restarting';
+ break;
+
+ default:
+ desc.state = fsm.state;
+ break;
+ }
+
+ return desc;
+ }
+
+ // -- implementation
+
+ var usedPorts = {}; // handle multiple attempts to bind to the same port
+ var spinnerByName = {}; // hash of all the spinners by name
+
+ function spinner(name) {
+ var fsm = spinnerByName[name];
+ if (fsm) return fsm;
+
+ var spinner = {
+
+ stopped: {
+ $enter: function(cb) {
+ spinner.qemit('stopped');
+ return cb();
+ },
+
+ start: function(cb) {
+ spinner.logger.info('starting', spinner.options.name);
+ spinner.state = 'start';
+ return cb();
+ },
+
+ stop: function(cb) {
+ spinner.logger.log('already stopped');
+ spinner.trigger('$enter');
+ return cb();
+ },
+ },
+
+ start: {
+ $enter: function(cb) {
+
+ function _findport(from, to, callback) {
+ spinner.logger.info('looking for an available port in the range:', [from, to]);
+ return portscanner.findAPortNotInUse(from, to, 'localhost', function(err, port) {
+ if (err) {
+ spinner.logger.error('unable to find available port for child', err);
+ return callback(err);
+ }
+
+ if (port in usedPorts) {
+ spinner.logger.info('Port ' + port + ' is already used, trying from ' + (port + 1));
+ return _findport(port + 1, to, callback);
+ }
+
+ usedPorts[port] = true;
+ return callback(null, port);
+ });
+ }
+
+ _findport(7000, 7999, function(err, port) {
+ if (err) return spinner.trigger('portNotFound');
+ else return spinner.trigger('portAllocated', port);
+ });
+
+ return cb();
+ },
+
+ portNotFound: function(cb) {
+ spinner.logger.error('out of ports... sorry... try again later');
+ spinner.state = 'faulted';
+ return cb();
+ },
+
+ portAllocated: function(cb, port) {
+ spinner.logger.info('found port', port);
+
+ // spawn the child process and store state
+ spinner.port = port;
+ spinner.logger.info('spawn', spinner.options.command, spinner.options.args);
+ var env = spinner.options.env || {};
+ env.port = env.PORT = spinner.port;
+ spinner.child = spawn(spinner.options.command, spinner.options.args, { env: env });
+ spinner.child.on('exit', function(code, signal) { return spinner.trigger('term', code, signal); });
+
+ spinner.child.stdout.on('data', function(data) { return spinner.emit('stdout', data); });
+ spinner.child.stderr.on('data', function(data) { return spinner.emit('stderr', data); });
+
+ spinner.state = 'wait';
+
+ return cb();
+ },
+
+ start: function(cb) {
+ spinner.logger.info('start already pending');
+ return cb();
+ },
+
+ term: 'faulted',
+ },
+
+ wait: {
+ $enter: function(cb) {
+ spinner.logger.info('waiting for port ' + spinner.port + ' to be bound');
+ spinner.wait.tries = spinner.options.timeout * 2;
+ spinner.wait.backoff = 500;
+
+ // will begin scanning
+ spinner.trigger('wait');
+
+ return cb();
+ },
+
+ wait: function(cb) {
+ spinner.logger.info('checking status of port ' + spinner.port, 'tries left:', spinner.wait.tries);
+
+ if (spinner.wait.tries-- === 0) {
+ spinner.logger.warn('timeout waiting for port');
+ return spinner.trigger('waitTimeout');
+ }
+
+ portscanner.checkPortStatus(spinner.port, 'localhost', function(err, status) {
+ spinner.logger.info('status is', status);
+ if (status === "open") return spinner.trigger("opened");
+ else return spinner.waitTimeout = spinner.timeout("wait", spinner.wait.backoff);
+ });
+
+ return cb();
+ },
+
+ term: function(cb) {
+ spinner.logger.error('process terminated while waiting');
+ spinner.state = 'faulted';
+ return cb();
+ },
+
+ waitTimeout: function(cb) {
+ spinner.logger.error('timeout waiting for port ' + spinner.port);
+ spinner.child.kill();
+ spinner.state = 'faulted';
+ return cb();
+ },
+
+ opened: function(cb) {
+ spinner.logger.info('port ' + spinner.port + ' opened successfuly');
+ spinner.state = 'started';
+ return cb();
+ },
+
+ start: function(cb) {
+ spinner.logger.info('start already pending');
+ return cb();
+ },
+
+ stop: function(cb) {
+ spinner.logger.info('stop waiting for child to start');
+ spinner.state = 'stop';
+ cb();
+ },
+
+ $exit: function(cb) {
+ if (spinner.waitTimeout) {
+ spinner.logger.info('clearing wait timeout');
+ clearTimeout(spinner.waitTimeout);
+ }
+ return cb();
+ },
+ },
+
+ faulted: {
+ $enter: function(cb) {
+ spinner._cleanup();
+
+ spinner.faulted.count = spinner.faulted.count ? spinner.faulted.count + 1 : 1;
+ spinner.logger.warn('faulted (' + spinner.faulted.count + '/' + spinner.options.attempts + ')');
+
+ if (spinner.faulted.count > spinner.options.attempts) {
+ spinner.logger.error('fault limit reached. staying in "faulted" state');
+ }
+ else {
+ spinner.logger.info('moving to stopped state');
+ spinner.state = 'stopped';
+ }
+
+ spinner.qemit('error', new Error("unable to start child process"));
+ return cb();
+ },
+
+ stop: function(cb) {
+ spinner.logger.info('moving to stop after faulted');
+ spinner.state = 'stopped';
+ spinner.faulted.count = 0; // reset fault caount
+ return cb();
+ },
+
+ start: function(cb) {
+ spinner.qemit('error', new Error('start failed to start for ' + spinner.options.attempts + ' times, stop before start'));
+ return cb();
+ },
+
+ '.*': function(cb, e) {
+ spinner.logger.error(e, 'triggered while in start-fault');
+ return cb();
+ },
+ },
+
+ started: {
+ $enter: function(cb) {
+ spinner.qemit('started', spinner.port);
+ cb();
+ },
+
+ start: function(cb) {
+ spinner.logger.info('already started');
+ spinner.trigger('$enter');
+ return cb();
+ },
+
+ stop: function(cb) {
+ spinner.logger.info('stopping');
+ spinner.state = 'stop'
+ return cb();
+ },
+
+ term: function(cb) {
+ spinner.logger.warn('child terminated unexpectedly, restarting');
+ spinner.state = 'restart';
+ return cb();
+ },
+ },
+
+ // helper function that cleans up the spinner in case we killed the process
+ _cleanup: function() {
+ spinner.child.removeAllListeners();
+ spinner.child = null;
+ delete usedPorts[spinner.port];
+ spinner.port = null;
+ },
+
+ stop: {
+ $enter: function(cb) {
+ spinner.child.kill();
+ spinner.stop.timeout = spinner.timeout('stopTimeout', spinner.options.stopTimeout * 1000);
+ cb();
+ },
+
+ term: function(cb, status) {
+ spinner.logger.info('exited with status', status);
+ spinner.exitStatus = status;
+ spinner.state = 'stopped';
+ return cb();
+ },
+
+ stopTimeout: function(cb) {
+ spinner.logger.info('stop timeout. sending SIGKILL');
+ spinner.child.kill('SIGKILL');
+ spinner.state = 'stopped';
+ return cb();
+ },
+
+ $exit: function(cb) {
+ spinner.logger.info('cleaning up');
+ clearTimeout(spinner.stop.timeout);
+ spinner._cleanup();
+ return cb();
+ },
+ },
+
+ restart: {
+ $enter: function(cb) {
+ spinner.logger.info('restarting');
+ spinner._cleanup();
+ spinner.state = 'start';
+ return cb();
+ },
+ },
+
+ assert: function(cb, err) {
+ spinner.logger.error('assertion', err);
+ return cb();
+ },
+ };
+
+ return spinnerByName[name] = fsmjs(spinner);
+ }
+
+ return api;
+};
View
1  main.js
@@ -0,0 +1 @@
+module.exports = require('./lib/spinner');
View
63 package.json
@@ -1,29 +1,36 @@
{
- "author": "George Stagas <gstagas@gmail.com> (http://stagas.com)",
- "author": "Elad Ben-Israel <eladb@microsoft.com>",
- "name": "spinner",
- "description": "Lazy spawn node apps using 'forever' and allocate ports dynamically ('nploy' fork)",
- "version": "0.1.5",
- "repository": {
- "type": "git",
- "url": "git://github.com/anodejs/spinner.git"
- },
- "engines": {
- "node": "~0.6.6"
- },
- "main": "./lib/spinner",
- "dependencies": {
- "forever": "~0.7.5",
- "async": "~0.1.15",
- "http-proxy": "~0.8.0",
- "atomic": "~0.0.2",
- "portscanner": "~0.1.2"
- },
- "devDependencies": {
- "request": "~2.2.9",
- "nodeunit": "~0.6.4"
- },
- "scripts": {
- "test": "nodeunit test/"
- }
-}
+ "name": "spinner",
+ "description": "Spawns child processes and allocates `process.env.PORT` for each.",
+ "main": "./main",
+ "author": "Elad Ben-Israel <elad.benisrael@gmail.com>",
+ "version": "0.2.0",
+ "contributors": [ ],
+ "keywords": [
+ "spawn",
+ "child",
+ "port",
+ "allocation"
+ ],
+ "dependencies": {
+ "portscanner": "0.1.x",
+ "fsmjs": "0.1.x",
+ "async": "0.1.x",
+ "ctxobj": "0.1.x"
+ },
+ "license": "MIT",
+ "engines": {
+ "node": "~0.6.6"
+ },
+ "devDependencies": {
+ "nodeunit": "0.6.x",
+ "logule": "0.5.x",
+ "request": "2.9.x"
+ },
+ "repository": {
+ "type": "git",
+ "url": "http://github.com/anodejs/node-spinner"
+ },
+ "scripts": {
+ "test": "nodeunit test/*.test.js"
+ }
+}
View
13 samples/basic.js
@@ -0,0 +1,13 @@
+// basic.js
+
+var request = require('request');
+var spinner = require('../main').createSpinner();
+
+spinner.start('lazykiller', function(err, port) {
+ if (err) return console.error(err);
+
+ request('http://localhost:' + port, function(err, res, body) {
+ console.log('response:', body);
+ //spinner.stopall();
+ });
+});
View
17 samples/lazykiller.js
@@ -0,0 +1,17 @@
+console.log('starting a');
+var http = require('http');
+http.createServer(function(req, res) {
+ res.end('this is a sample app');
+}).listen(process.env.PORT, function(err) {
+ if (err) return console.error(err);
+ return console.info('started on port ' + process.env.PORT);
+});
+
+// crash process after 10s
+var i = 5;
+setInterval(function() {
+ if (i == 0) throw new Error();
+ console.log('dead in ' + i + 'sec');
+ i--;
+}, 1000);
+
View
8 samples/myapp.js
@@ -0,0 +1,8 @@
+console.log('starting a');
+var http = require('http');
+http.createServer(function(req, res) {
+ res.end('this is a sample app');
+}).listen(process.env.PORT, function(err) {
+ if (err) return console.error(err);
+ return console.info('started on port ' + process.env.PORT);
+});
View
301 test/all.test.js
@@ -0,0 +1,301 @@
+var path = require('path');
+var fs = require('fs');
+var request = require('request');
+var ctxobj = require('ctxobj');
+var async = require('async');
+var fsmjs = require('fsmjs');
+var debug = fsmjs.debug;
+var console = ctxobj.console(require('logule')).stacktop();
+
+// set this to 'true' and an interactive debugger will be attached.
+var interactiveDebugger = false;
+
+//
+// read all scripts from ./scripts directory and create a hash
+// of all of them, with indication of whether they should succeed or fail
+//
+
+var scriptsDir = path.join(__dirname, '../test/scripts');
+console.log("scripts directory:", scriptsDir);
+var testScripts = {};
+fs.readdirSync(scriptsDir)
+ .map(function(f) {
+ var desc = { name: f, fail: false };
+ if (f[0] === ".") return null; // filter files that start with '.'
+ if (!!~f.indexOf('.fail')) desc.fail = true;
+ return desc;
+ })
+ .filter(function(d) { return d; })
+ .forEach(function(d) { testScripts[d.name] = d; });
+
+
+var tests = {};
+
+// setup code for all tests
+tests.setUp = function(cb) {
+ var self = this;
+
+ self.spinner = require('../main').createSpinner();
+
+ this.spin = function(fileName, callback) {
+ console.log('spinning', fileName);
+
+ var script = path.join(scriptsDir, fileName);
+ var options = {
+ name: fileName,
+ script: script,
+ logger: console,
+ timeout: 10,
+ attempts: 7 // 7 start attempts before staying in 'faulted' state
+ };
+
+ var child = self.spinner.start(options, function(err, port) {
+ if (err) console.log(fileName, 'error spinning');
+ else console.log(fileName, 'ready on port', port);
+
+ return callback(err, port);
+ });
+
+ //child.on('stdout', function(data) { console.info('STDOUT:', data.toString()); });
+ //child.on('stderr', function(data) { console.error('STDERR:', data.toString()); });
+
+ debug(child, { verbose: true, logger: console, logonly: !interactiveDebugger });
+
+ return child;
+ };
+
+ this.stop = function(fileName, callback) {
+ console.log('stopping', fileName);
+ return self.spinner.stop(fileName, callback);
+ };
+
+ this.spinstop = function(fileName, callback) {
+ self.spin(fileName, function(err, port) {
+ if (err) return callback(new Error("unable to start: " + err.toString()));
+
+ self.stop(fileName, function(err) {
+ if (err) return callback(new Error("unable to stop: " + e.toString()));
+ callback(null, port);
+ });
+ });
+ };
+
+ cb();
+};
+
+// teardown code for all tests
+tests.tearDown = function(cb) {
+ var self = this;
+
+ // make sure all children are stopped
+
+ var children = self.spinner.list();
+ for (var n in children) {
+ var child = children[n];
+ var desc = testScripts[n];
+ if (child.state !== "stopped") {
+ console.error("child " + child.name + " not stopped", child.state);
+ }
+ }
+
+ return cb();
+};
+
+
+//
+// spin an app and make sure we can send it a request
+//
+
+tests.normal = function(test) {
+ var self = this;
+ self.spin('normal.js', function(err, port) {
+ test.ok(!err);
+ test.ok(port);
+ request('http://localhost:' + port, function(err, res, body) {
+ test.ok(!err);
+ test.equals(body, 'this is a sample app');
+
+ // stop it
+ self.stop('normal.js', function(err) {
+ test.done();
+ });
+ });
+ });
+};
+
+//
+// start twice and expect the same port to be returned
+//
+
+tests.startTwice = function(test) {
+ var self = this;
+
+ self.spin('normal.js', function(err, port) {
+ test.ok(!err, err);
+ test.ok(port);
+
+ self.spin('normal.js', function(err, port2) {
+ test.ok(!err, err);
+ test.equals(port, port2);
+
+ self.stop('normal.js', function() {
+ test.done();
+ });
+ });
+ });
+};
+
+//
+// just try to spin a.js 100 times.
+//
+
+tests.multi = function(test) {
+ var self = this;
+ var array = []; for (var i = 0; i < 100; ++i) array.push(i);
+ async.forEachSeries(array, function(i, cb) {
+ self.spin('a.js', cb);
+ }, function(err, ports) {
+ self.stop('a.js', function() {
+ test.done();
+ });
+ });
+};
+
+//
+// start an app that takes a few seconds to bing
+// this should work well as it is in the alloted start timeout
+//
+
+tests.stalling = function(test) {
+ this.spinstop('stalling.js', function(err, port) {
+ test.ok(!err, err);
+ test.ok(port);
+ test.done();
+ })
+};
+
+//
+// try to start an app that fails during load
+// expect an error.
+//
+
+tests.loadfail = function(test) {
+ var self = this;
+ self.spin('load.fail.js', function(err, port) {
+ test.ok(err);
+
+ // stop it now so it will go back to stopped state
+ self.stop('load.fail.js', function(err) {
+ test.done();
+ });
+ });
+}
+
+//
+// reach 'faulted' state by trying to spin an app
+// that fails to start more than 7 times (options.attempts)
+//
+
+tests.faulted = function(test) {
+ var self = this;
+ var numberOfStartRequests = 15;
+ var range = [];
+
+ for (i = 0; i < numberOfStartRequests; ++i) range.push(i);
+
+ var outputStates = [];
+
+ async.forEachSeries(range, function(i, cb) {
+
+ self.spin('load.fail.js', function(err, port) {
+ test.ok(err, "start should fail");
+
+ var outputState = self.spinner.get('load.fail.js').state;
+ outputStates.push(outputState);
+
+ cb();
+ });
+
+ }, function() {
+ var sum = { 'stopped': 0, 'faulted': 0 };
+ outputStates.map(function(s) { sum[s]++; });
+ test.equals(sum['stopped'], 7, "since we configured 7 attempts");
+ test.equals(sum['faulted'], 8, "15-7");
+ test.done();
+ });
+};
+
+//
+// run a crazy stress test
+// that basically spins random scripts and expects
+// everything to be okay. this can get even crazier...
+//
+
+tests.stress = function(test) {
+ var self = this;
+
+ var totalRequests = 128;
+ var scripts = [];
+
+ for (var i = 0; i < totalRequests; ++i) {
+ var index = Math.round(Math.random() * 10000) % Object.keys(testScripts).length;
+ var s = testScripts[Object.keys(testScripts)[index]];
+ scripts.push(s);
+ }
+
+ // collect some stats to set expecations
+ var requestsPerScript = {};
+ var numberOfScripts = 0;
+ var expectedSuccesses = 0;
+
+ scripts.forEach(function(s) {
+ requestsPerScript[s.name] = requestsPerScript[s.name] ? requestsPerScript[s.name] + 1 : 1;
+ expectedSuccesses += s.fail ? 0 : 1;
+ });
+
+ numberOfScripts = Object.keys(requestsPerScript).length;
+
+ console.log('number of scripts:', numberOfScripts);
+ console.log('requests per script:', requestsPerScript);
+
+ var successCount = 0;
+ var failureCount = 0;
+ var ports = {};
+ var portsPerScript = {};
+
+ async.forEach(scripts, function(s, cb) {
+
+ self.spin(s.name, function(err, port) {
+ //console.log('SPIN DONE', s, err, port, cb);
+ if (err) {
+ failureCount++;
+ return cb();
+ }
+
+ successCount++;
+ if (!portsPerScript[s]) portsPerScript[s] = {};
+ portsPerScript[s][port] = true;
+ ports[port] = true;
+
+ return cb();
+ });
+
+ }, function(err) {
+ test.ok(!err, err);
+
+ console.log('Number of success starts:', successCount);
+ console.log('Number of failed starts:', failureCount);
+
+ test.deepEqual(successCount, expectedSuccesses, "make sure we have the correct number of successes");
+ test.deepEqual(failureCount, totalRequests - expectedSuccesses, "failure count is the remainder");
+
+ console.log("All scripts started: ", ports);
+ console.log("Stopping all scripts");
+ self.spinner.stopall(function() {
+ console.log('all stopped');
+ test.done();
+ });
+ });
+};
+
+exports.tests = require('nodeunit').testCase(tests);
View
8 test/scripts/a.js
@@ -0,0 +1,8 @@
+console.log('starting a');
+var http = require('http');
+http.createServer(function(req, res) {
+ res.end('this is a sample app');
+}).listen(process.env.PORT, function(err) {
+ if (err) return console.error(err);
+ return console.info('started on port ' + process.env.PORT);
+});
View
17 test/scripts/lazykiller.js
@@ -0,0 +1,17 @@
+console.log('starting a');
+var http = require('http');
+http.createServer(function(req, res) {
+ res.end('this is a sample app');
+}).listen(process.env.PORT, function(err) {
+ if (err) return console.error(err);
+ return console.info('started on port ' + process.env.PORT);
+});
+
+// crash process after 10s
+var i = 5;
+setInterval(function() {
+ if (i == 0) throw new Error();
+ console.log('dead in ' + i + 'sec');
+ i--;
+}, 1000);
+
View
1  test/scripts/load.fail.js
@@ -0,0 +1 @@
+throw new Error("ha ha, no way!");
View
8 test/scripts/normal.js
@@ -0,0 +1,8 @@
+console.log('starting a');
+var http = require('http');
+http.createServer(function(req, res) {
+ res.end('this is a sample app');
+}).listen(process.env.PORT, function(err) {
+ if (err) return console.error(err);
+ return console.info('started on port ' + process.env.PORT);
+});
View
12 test/scripts/stalling.js
@@ -0,0 +1,12 @@
+console.log('starting process.... it will take about 5 seconds');
+
+setTimeout(function() {
+
+ var http = require('http');
+ http.createServer(function(req, res) {
+ res.end('here');
+ }).listen(process.env.PORT);
+
+ console.log('listening on port', process.env.PORT);
+
+}, 2000);
Please sign in to comment.
Something went wrong with that request. Please try again.