diff --git a/doc/api/cluster.markdown b/doc/api/cluster.markdown index b4e6a3b25e..b2a23e0cdc 100644 --- a/doc/api/cluster.markdown +++ b/doc/api/cluster.markdown @@ -148,7 +148,8 @@ When any of the workers die the cluster module will emit the 'exit' event. This can be used to restart the worker by calling `fork()` again. cluster.on('exit', function(worker) { - console.log('worker ' + worker.pid + ' died. restart...'); + var exitCode = worker.process.exitCode; + console.log('worker ' + worker.pid + ' died ('+exitCode+'). restarting...'); cluster.fork(); }); @@ -436,11 +437,20 @@ on the specified worker. ### Event: 'exit' -* `worker` {Worker object} - -Same as the `cluster.on('exit')` event, but emits only when the state change -on the specified worker. - - cluster.fork().on('exit', function (worker) { - // Worker has died +* `code` {Number} the exit code, if it exited normally. +* `signal` {String} the name of the signal (eg. `'SIGHUP'`) that caused + the process to be killed. + +Emitted by the individual worker instance, when the underlying child process +is terminated. See [child_process event: 'exit'](child_process.html#child_process_event_exit). + + var worker = cluster.fork(); + worker.on('exit', function (code, signal) { + if( signal ) { + console.log("worker was killed by signal: "+signal); + } else if( code !== 0 ) { + console.log("worker exited with error code: "+code); + } else { + console.log("worker success!"); + } }; diff --git a/lib/cluster.js b/lib/cluster.js index 504c399ef7..b2173f0b6c 100644 --- a/lib/cluster.js +++ b/lib/cluster.js @@ -294,9 +294,16 @@ function Worker(customEnv) { // handle internalMessage, exit and disconnect event this.process.on('internalMessage', handleMessage.bind(null, this)); - this.process.on('exit', prepareExit.bind(null, this, 'dead', 'exit')); - this.process.on('disconnect', - prepareExit.bind(null, this, 'disconnected', 'disconnect')); + this.process.once('exit', function(exitCode, signalCode) { + prepareExit(self, 'dead'); + self.emit('exit', exitCode, signalCode); + cluster.emit('exit', self); + }); + this.process.once('disconnect', function() { + prepareExit(self, 'disconnected'); + self.emit('disconnect'); + cluster.emit('disconnect', self); + }); // relay message and error this.process.on('message', this.emit.bind(this, 'message')); @@ -306,7 +313,7 @@ function Worker(customEnv) { util.inherits(Worker, EventEmitter); cluster.Worker = Worker; -function prepareExit(worker, state, eventName) { +function prepareExit(worker, state) { // set state to disconnect worker.state = state; @@ -318,10 +325,6 @@ function prepareExit(worker, state, eventName) { if (cluster.isMaster) { delete cluster.workers[worker.uniqueID]; } - - // Emit events - worker.emit(eventName, worker); - cluster.emit(eventName, worker); } // Send internal message diff --git a/test/simple/test-cluster-basic.js b/test/simple/test-cluster-basic.js index 3fe5f7fc27..f20636a8e8 100644 --- a/test/simple/test-cluster-basic.js +++ b/test/simple/test-cluster-basic.js @@ -122,7 +122,15 @@ else if (cluster.isMaster) { checks.worker.events[name] = true; //Check argument - checks.worker.equal[name] = worker === arguments[0]; + if (name == 'exit') { + checks.worker.equal[name] = ( + worker.process.exitCode === arguments[0] && + worker.process.signalCode === arguments[1] && + worker === this + ); + } else { + checks.worker.equal[name] = worker === arguments[0]; + } }); }); diff --git a/test/simple/test-cluster-worker-death.js b/test/simple/test-cluster-worker-death.js index 41dbfc58b3..ccd48552ab 100644 --- a/test/simple/test-cluster-worker-death.js +++ b/test/simple/test-cluster-worker-death.js @@ -30,11 +30,12 @@ else { var seenExit = 0; var seenDeath = 0; var worker = cluster.fork(); - worker.on('exit', function(statusCode) { - assert.equal(statusCode, 42); + worker.on('exit', function(exitCode, signalCode) { + assert.equal(exitCode, 42); + assert.equal(signalCode, null); seenExit++; }); - cluster.on('death', function(worker_) { + cluster.on('exit', function(worker_) { assert.equal(worker_, worker); seenDeath++; }); diff --git a/test/simple/test-cluster-worker-exit.js b/test/simple/test-cluster-worker-exit.js new file mode 100644 index 0000000000..38b44c199c --- /dev/null +++ b/test/simple/test-cluster-worker-exit.js @@ -0,0 +1,152 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// 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. + + +// test-cluster-worker-exit.js +// verifies that, when a child process exits (by calling `process.exit(code)`) +// - the parent receives the proper events in the proper order, no duplicates +// - the exitCode and signalCode are correct in the 'exit' event +// - the worker.suicide flag, and worker.state are correct +// - the worker process actually goes away + +var common = require('../common'); +var assert = require('assert'); +var cluster = require('cluster'); + +var EXIT_CODE = 42; + +if (cluster.isWorker) { + var http = require('http'); + var server = http.Server(function() { }); + + server.once('listening', function() { + process.exit(EXIT_CODE); + }); + server.listen(common.PORT, '127.0.0.1'); + +} else if (cluster.isMaster) { + + var expected_results = { + cluster_emitDisconnect: [1, "the cluster did not emit 'disconnect'"], + cluster_emitExit: [1, "the cluster did not emit 'exit'"], + cluster_exitCode: [EXIT_CODE, 'the cluster exited w/ incorrect exitCode'], + cluster_signalCode: [null, 'the cluster exited w/ incorrect signalCode'], + worker_emitDisconnect: [1, "the worker did not emit 'disconnect'"], + worker_emitExit: [1, "the worker did not emit 'exit'"], + worker_state: ['disconnected', 'the worker state is incorrect'], + worker_suicideMode: [false, 'the worker.suicide flag is incorrect'], + worker_died: [true, 'the worker is still running'], + worker_exitCode: [EXIT_CODE, 'the worker exited w/ incorrect exitCode'], + worker_signalCode: [null, 'the worker exited w/ incorrect signalCode'] + }; + var results = { + cluster_emitDisconnect: 0, + cluster_emitExit: 0, + worker_emitDisconnect: 0, + worker_emitExit: 0 + }; + + + // start worker + var worker = cluster.fork(); + + worker.once('listening', function() { + // the worker is up and running... + }); + + + // Check cluster events + cluster.on('disconnect', function() { + results.cluster_emitDisconnect += 1; + }); + cluster.on('exit', function(worker) { + results.cluster_exitCode = worker.process.exitCode; + results.cluster_signalCode = worker.process.signalCode; + results.cluster_emitExit += 1; + assert.ok(results.cluster_emitDisconnect, + "cluster: 'exit' event before 'disconnect' event"); + }); + + // Check worker eventes and properties + worker.on('disconnect', function() { + results.worker_emitDisconnect += 1; + results.worker_suicideMode = worker.suicide; + results.worker_state = worker.state; + }); + + // Check that the worker died + worker.once('exit', function(exitCode, signalCode) { + results.worker_exitCode = exitCode; + results.worker_signalCode = signalCode; + results.worker_emitExit += 1; + results.worker_died = !alive(worker.process.pid); + assert.ok(results.worker_emitDisconnect, + "worker: 'exit' event before 'disconnect' event"); + + process.nextTick(function() { finish_test(); }); + }); + + var finish_test = function() { + try { + checkResults(expected_results, results); + } catch (exc) { + console.error('FAIL: ' + exc.message); + if (exc.name != 'AssertionError') { + console.trace(exc); + } + + process.exit(1); + return; + } + process.exit(0); + }; +} + +// some helper functions ... + + function checkResults(expected_results, results) { + for (var k in expected_results) { + var actual = results[k], + expected = expected_results[k]; + + if (typeof expected === 'function') { + expected(r[k]); + } else { + var msg = (expected[1] || '') + + (' [expected: ' + expected[0] + ' / actual: ' + actual + ']'); + + if (expected && expected.length) { + assert.equal(actual, expected[0], msg); + } else { + assert.equal(actual, expected, msg); + } + } + } + } + + function alive(pid) { + try { + process.kill(pid, 'SIGCONT'); + return true; + } catch (e) { + return false; + } + } diff --git a/test/simple/test-cluster-worker-kill.js b/test/simple/test-cluster-worker-kill.js new file mode 100644 index 0000000000..9fd13ad65e --- /dev/null +++ b/test/simple/test-cluster-worker-kill.js @@ -0,0 +1,149 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// 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. + + +// test-cluster-worker-kill.js +// verifies that, when a child process is killed (we use SIGHUP) +// - the parent receives the proper events in the proper order, no duplicates +// - the exitCode and signalCode are correct in the 'exit' event +// - the worker.suicide flag, and worker.state are correct +// - the worker process actually goes away + +var common = require('../common'); +var assert = require('assert'); +var cluster = require('cluster'); + +if (cluster.isWorker) { + var http = require('http'); + var server = http.Server(function() { }); + + server.once('listening', function() { }); + server.listen(common.PORT, '127.0.0.1'); + +} else if (cluster.isMaster) { + + var KILL_SIGNAL = 'SIGHUP', + expected_results = { + cluster_emitDisconnect: [1, "the cluster did not emit 'disconnect'"], + cluster_emitExit: [1, "the cluster did not emit 'exit'"], + cluster_exitCode: [null, 'the cluster exited w/ incorrect exitCode'], + cluster_signalCode: [KILL_SIGNAL, 'the cluster exited w/ incorrect signalCode'], + worker_emitDisconnect: [1, "the worker did not emit 'disconnect'"], + worker_emitExit: [1, "the worker did not emit 'exit'"], + worker_state: ['disconnected', 'the worker state is incorrect'], + worker_suicideMode: [false, 'the worker.suicide flag is incorrect'], + worker_died: [true, 'the worker is still running'], + worker_exitCode: [null, 'the worker exited w/ incorrect exitCode'], + worker_signalCode: [KILL_SIGNAL, 'the worker exited w/ incorrect signalCode'] + }, + results = { + cluster_emitDisconnect: 0, + cluster_emitExit: 0, + worker_emitDisconnect: 0, + worker_emitExit: 0 + }; + + + // start worker + var worker = cluster.fork(); + + // when the worker is up and running, kill it + worker.once('listening', function() { + worker.process.kill(KILL_SIGNAL); + }); + + + // Check cluster events + cluster.on('disconnect', function() { + results.cluster_emitDisconnect += 1; + }); + cluster.on('exit', function(worker) { + results.cluster_exitCode = worker.process.exitCode; + results.cluster_signalCode = worker.process.signalCode; + results.cluster_emitExit += 1; + assert.ok(results.cluster_emitDisconnect, + "cluster: 'exit' event before 'disconnect' event"); + }); + + // Check worker eventes and properties + worker.on('disconnect', function() { + results.worker_emitDisconnect += 1; + results.worker_suicideMode = worker.suicide; + results.worker_state = worker.state; + }); + + // Check that the worker died + worker.once('exit', function(exitCode, signalCode) { + results.worker_exitCode = exitCode; + results.worker_signalCode = signalCode; + results.worker_emitExit += 1; + results.worker_died = !alive(worker.process.pid); + assert.ok(results.worker_emitDisconnect, + "worker: 'exit' event before 'disconnect' event"); + + process.nextTick(function() { finish_test(); }); + }); + + var finish_test = function() { + try { + checkResults(expected_results, results); + } catch (exc) { + console.error('FAIL: ' + exc.message); + if (exc.name != 'AssertionError') { + console.trace(exc); + } + + process.exit(1); + return; + } + process.exit(0); + }; +} + +// some helper functions ... + + function checkResults(expected_results, results) { + for (var k in expected_results) { + var actual = results[k], + expected = expected_results[k]; + + if (typeof expected === 'function') { + expected(r[k]); + } else { + var msg = (expected[1] || '') + + (' [expected: ' + expected[0] + ' / actual: ' + actual + ']'); + if (expected && expected.length) { + assert.equal(actual, expected[0], msg); + } else { + assert.equal(actual, expected, msg); + } + } + } + } + + function alive(pid) { + try { + process.kill(pid, 'SIGCONT'); + return true; + } catch (e) { + return false; + } + }