cluster: worker exit event to match child_process
authorJ. Lee Coltrane <lee@projectmastermind.com>
Wed, 2 May 2012 16:38:31 +0000 (12:38 -0400)
committerisaacs <i@izs.me>
Sat, 5 May 2012 00:28:21 +0000 (17:28 -0700)
test: fixes due to new cluster api.

- changed worker `death` to `exit`.
- corrected argument type expected by worker `exit` handler.

test: more tests of cluster.worker death

cluster: fixed arguments on worker 'exit' event

worker 'exit' event now emits arguments consistent with the
corresponding event in child_process module.

doc/api/cluster.markdown
lib/cluster.js
test/simple/test-cluster-basic.js
test/simple/test-cluster-worker-death.js
test/simple/test-cluster-worker-exit.js [new file with mode: 0644]
test/simple/test-cluster-worker-kill.js [new file with mode: 0644]

index b4e6a3b..b2a23e0 100644 (file)
@@ -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!");
+      }
     };
index 504c399..b2173f0 100644 (file)
@@ -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
index 3fe5f7f..f20636a 100644 (file)
@@ -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];
+      }
     });
   });
 
index 41dbfc5..ccd4855 100644 (file)
@@ -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 (file)
index 0000000..38b44c1
--- /dev/null
@@ -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 (file)
index 0000000..9fd13ad
--- /dev/null
@@ -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;
+    }
+  }