Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exponentially backed-off respawns with maximum attempts #60

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ parameters:
- `title`: (`String`): see `--title` above.
- `assumeReady`: (`Boolean`): see Worker readiness below.
- `keepAlive`: (`Boolean`): see `--keepalive` above.
- `backoffRespawns`: null to disable exponential backoff respawns. -1 to disable maximum respawns, and any positive int to set a limit on attempted respawns
- `backoffDelay`: delay to first respawn attempt in milliseconds
- `backoffMaxDelay`: the maximum delay between respawn attempts in milliseconds
- `minExpectedLifetime`: (`Number`|`String`): Number of ms a worker is
expected to live. Don't auto-respawn if a worker dies earlier. Strings
like `'10s'` are accepted. Defaults to `'20s'`.
Expand Down
2 changes: 1 addition & 1 deletion examples/hello-world/up.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

var server = require('http').Server().listen(3000)

require('../../up')(server, __dirname + '/server')
require('../../../up')(server, __dirname + '/server')
80 changes: 75 additions & 5 deletions lib/up.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var fork = require('child_process').fork
, eq = require('eq')
, os = require('os')
, ms = require('ms')
, backoff = require('backoff')
, env = process.env.NODE_ENV
, Distributor = require('distribute')
, EventEmitter = require('events').EventEmitter
Expand Down Expand Up @@ -83,6 +84,9 @@ function UpServer (server, file, opts) {
this.assumeReady = opts.assumeReady === undefined ? true : !!opts.assumeReady;
this.keepAlive = opts.keepAlive || false;
this.minExpectedLifetime = ms(opts.minExpectedLifetime != null ? opts.minExpectedLifetime : minExpectedLifetime);
this.backoffRespawns = opts.backoffRespawns || null;
this.backoffInitDelay = opts.backoffInitDelay || 100;
this.backoffMaxDelay = opts.backoffMaxDelay || 10000;
if (false !== opts.workerPingInterval) {
this.workerPingInterval = ms(opts.workerPingInterval || '1m');
}
Expand Down Expand Up @@ -188,9 +192,10 @@ UpServer.prototype.spawnWorkers = function (n) {
* @api public
*/

UpServer.prototype.spawnWorker = function (fn) {
UpServer.prototype.spawnWorker = function (fn, dontrespawn) {
var w = new Worker(this)
, self = this
, previousState = w.readyState;

// keep track that we're spawning
this.spawning.push(w);
Expand All @@ -200,7 +205,14 @@ UpServer.prototype.spawnWorker = function (fn) {
case 'spawned':
self.spawning.splice(self.spawning.indexOf(w), 1);
self.workers.push(w);
fn && fn(w.readyState);
self.emit('spawn', w);
// wait until minExpectedLifetime has been reached so that failed starts can be restarted
setTimeout(function() {
if (w.readyState == 'terminating' || w.readyState == 'terminated') {
self.emit('unsuccessful', w);
}
}, self.minExpectedLifetime-1);
break;

case 'terminating':
Expand All @@ -212,21 +224,69 @@ UpServer.prototype.spawnWorker = function (fn) {
self.workers.splice(self.workers.indexOf(w), 1);
self.lastIndex = -1;
if (self.keepAlive && (self.workers.length + self.spawning.length < self.numWorkers)) {
if (new Date().getTime() - w.birthtime < self.minExpectedLifetime) {
debug('worker %s found dead at a too young age. won\'t respawn new worker', w.pid);
if (w.uptime() < self.minExpectedLifetime) {
debug('worker %s found dead at a too young an age. won\'t respawn new worker', w.pid);
}
else {
debug('worker %s found dead. spawning 1 new worker', w.pid);
self.spawnWorker();
if (!dontrespawn) self.respawnWorker();
}
}
fn && fn(w.readyState);
}
else if (previousState == 'spawning' && w.proc.exitCode > 0) {
if (self.minExpectedLifetime == 0 && self.keepAlive && (self.workers.length + self.spawning.length < self.numWorkers)) { // special case
debug('worker %s found dead. spawning 1 new worker', w.pid);
if (!dontrespawn) self.respawnWorker();
}
else {
self.emit('unsuccessful', w);
}
}
self.emit('terminate', w)
self.emit('terminate', w);
break;
}
previousState = w.readyState;
});
};

/**
* Spawns a worker that binds to an available port.
*
* @api private
*/

UpServer.prototype.respawnWorker = function () {
var self = this;

self.emit('respawn');

if (self.backoffRespawns !== null) {
var respawnBackoff = backoff.exponential({
initialDelay: self.backoffInitDelay
, maxDelay: self.backoffMaxDelay
})
respawnBackoff.on('ready', function(number, delay) {
self.spawnWorker(function(readyState) {
if (readyState == 'terminated') {
self.emit('respawn');
respawnBackoff.backoff();
}
}, true);
})
respawnBackoff.on('fail', function() {
self.emit('respawnerror');
});
if (self.backoffRespawns > 0) {
respawnBackoff.failAfter(self.backoffRespawns-1);
}
respawnBackoff.backoff();
}
else {
self.spawnWorker(null, true);
}
};

/**
* Gets the next port in the round.
*
Expand Down Expand Up @@ -373,6 +433,16 @@ Worker.prototype.shutdown = function () {
}
};

/**
* Uptime of current worker
*
* @api public
*/

Worker.prototype.uptime = function() {
return new Date().getTime() - this.birthtime;
};

/**
* Send ready signal from within a worker
*
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
, "ms": "0.1.0"
, "debug": "0.1.0"
, "commander": "0.6.1"
, "backoff": "2.0.0"
, "distribute": "0.1.4"
}
, "devDependencies": {
Expand Down
4 changes: 4 additions & 0 deletions test/child-backoff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

var httpServer = require('http').Server()
, up = require('../lib/up')(httpServer, __dirname + '/child-server-backoff'
, { workerPingInterval: '15ms', numWorkers: 1, keepAlive:true, minExpectedLifetime:0, backoffRespawns:-1, backoffInitDelay:10, backoffMaxDelay:1000 })
6 changes: 6 additions & 0 deletions test/child-server-backoff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

var client = require('net').connect(7003, function () {
client.write(String(process.pid));
});

module.exports = require('http').Server()
3 changes: 3 additions & 0 deletions test/server-asyncfail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
var server = require('./server');
module.exports = server;
setTimeout(function() { process.exit(1); }, 100);
2 changes: 2 additions & 0 deletions test/server-fail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

process.exit(1);
2 changes: 1 addition & 1 deletion test/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ var express = require('express')
* Initialize server
*/

var app = express.createServer();
var app = express();

/**
* Default route.
Expand Down
Loading