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

Enhanced signal support #22

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions lib/throng.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ module.exports = function throng(options, startFunction) {
function listen() {
cluster.on('exit', revive);
emitter.once('shutdown', shutdown);
process
.on('SIGINT', proxySignal)
.on('SIGTERM', proxySignal);
var shutdownSignals = ['SIGINT', 'SIGTERM'];
if (opts.extraShutdownSignal) shutdownSignals.push(opts.extraShutdownSignal);
shutdownSignals.forEach((signal) => {
process.once(signal, () => emitter.emit('shutdown', signal));
});
}

function fork() {
Expand All @@ -49,16 +51,20 @@ module.exports = function throng(options, startFunction) {
}
}

function proxySignal() {
emitter.emit('shutdown');
}

function shutdown() {
function shutdown(signal) {
running = false;
for (var id in cluster.workers) {
cluster.workers[id].process.kill();
}

// Unref'ing the timer ensures that the only thing keeping the master alive
// (as far as this module knows) are the connections to the child processes.
// When they disconnect (either because they exit voluntarily or because
// we disconnect them with `Worker#kill`), we'll receive the 'exit' event.
// Kill with the shutdown signal to respect signal exit codes:
// https://nodejs.org/api/process.html#process_exit_codes
setTimeout(forceKill, opts.grace).unref();
process.on('exit', () => process.kill(process.pid, signal));
}

function revive(worker, code, signal) {
Expand Down
12 changes: 7 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,13 @@ Handling signals (for cleanup on a kill signal, for instance).

```js
throng({
workers: 4, // Number of workers (cpu count)
lifetime: 10000, // ms to keep cluster alive (Infinity)
grace: 4000, // ms grace period after worker SIGTERM (5000)
master: masterFn, // Function to call when starting the master process
start: startFn // Function to call when starting the worker processes
workers: 4, // Number of workers (cpu count)
lifetime: 10000, // ms to keep cluster alive (Infinity)
grace: 4000, // ms grace period after worker SIGTERM (5000)
master: masterFn, // Function to call when starting the master process
start: startFn, // Function to call when starting the worker processes
extraShutdownSignal: 'SIGUSR2' // Extra signal (beyond SIGINT & SIGTERM) in response
// to which to shut down the cluster
});
```

Expand Down
17 changes: 17 additions & 0 deletions test/fixtures/sigusr2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

const throng = require('../../lib/throng');

throng({
workers: 3,
lifetime: 500,
start: () => {
console.log('worker');

process.on('SIGTERM', function() {
console.log('exiting');
process.exit();
});
},
extraShutdownSignal: 'SIGUSR2'
});
67 changes: 65 additions & 2 deletions test/throng.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const masterCmd = path.join(__dirname, 'fixtures', 'master');
const gracefulCmd = path.join(__dirname, 'fixtures', 'graceful');
const killCmd = path.join(__dirname, 'fixtures', 'kill');
const infiniteCmd = path.join(__dirname, 'fixtures', 'infinite');
const sigusr2Cmd = path.join(__dirname, 'fixtures', 'sigusr2');

describe('throng()', function() {

Expand Down Expand Up @@ -92,7 +93,7 @@ describe('throng()', function() {
});
});

describe('signal handling', function() {
describe('signal handling in workers', function() {

describe('with 3 workers that exit gracefully', function() {
before(function(done) {
Expand Down Expand Up @@ -128,6 +129,67 @@ describe('throng()', function() {
});

});

describe('signal handling in master', function() {

describe('with SIGTERM (default)', function() {
before(function(done) {
var child = run(gracefulCmd, this, done);
setTimeout(function() { child.kill(); }, 750);
});
it('allows the workers to shut down', function() {
var exits = this.stdout.match(/exiting/g).length;
assert.equal(exits, 3);
});
it('exits with SIGTERM', function() {
assert.equal(this.signal, 'SIGTERM');
});
});

describe('with workers that fail to exit', function() {
before(function(done) {
var child = run(killCmd, this, done);
setTimeout(function() { child.kill(); }, 750);
});
it('notifies the workers that they should exit', function() {
var exits = this.stdout.match(/stayin alive/g).length;
assert.equal(exits, 3);
});
it('exits with SIGTERM', function() {
assert.equal(this.signal, 'SIGTERM');
});
});

describe('with SIGINT (default)', function() {
before(function(done) {
var child = run(gracefulCmd, this, done);
setTimeout(function() { child.kill('SIGINT'); }, 750);
});
it('allows the workers to shut down', function() {
var exits = this.stdout.match(/exiting/g).length;
assert.equal(exits, 3);
});
it('exits with SIGINT', function() {
assert.equal(this.signal, 'SIGINT');
});
});

describe('with custom signal handling', function() {
before(function(done) {
var child = run(sigusr2Cmd, this, done);
setTimeout(function() { child.kill('SIGUSR2'); }, 750);
});
it('allows the workers to shut down', function() {
var exits = this.stdout.match(/exiting/g).length;
assert.equal(exits, 3);
});
it('exits with SIGUSR2', function() {
assert.equal(this.signal, 'SIGUSR2');
});
});

});

});

function run(file, context, done) {
Expand All @@ -140,8 +202,9 @@ function run(file, context, done) {
child.stderr.on('data', function(data) {
context.stdout += data.toString();
});
child.on('close', function(code) {
child.on('close', function(code, signal) {
context.endTime = Date.now();
context.signal = signal;
done();
});
return child;
Expand Down