Add cluster.setupMaster
authorAndreas Madsen <amwebdk@gmail.com>
Thu, 5 Jan 2012 19:09:43 +0000 (20:09 +0100)
committerRyan Dahl <ry@tinyclouds.org>
Fri, 20 Jan 2012 21:09:56 +0000 (13:09 -0800)
Fixes #2470

doc/api/cluster.markdown
lib/cluster.js
test/simple/test-cluster-kill-workers.js [deleted file]
test/simple/test-cluster-master-error.js [new file with mode: 0644]
test/simple/test-cluster-setup-master.js [new file with mode: 0644]

index 96e4ef8f37a9cbc97be7f0cb095f3122e5b267fa..d5916e6028c8ce4d6ab3fc386cfd97cf70ca6d51 100644 (file)
@@ -101,6 +101,40 @@ This can be used to restart the worker by calling `fork()` again.
       cluster.fork();
     });
 
+### Event 'setup'
+
+When the `.setupMaster()` function has been executed this event emits. If `.setupMaster()`
+was not executed before `fork()` or `.autoFork()`, they will execute the function with no
+arguments.
+
+### cluster.setupMaster([options])
+
+The `setupMaster` is used to change the default 'fork' behavior. It takes one option
+object argument.
+
+Example:
+
+    var cluster = require("cluster");
+    cluster.setupMaster({
+      exec : "worker.js",
+      args : ["--use", "https"],
+      silent : true
+    });
+    cluster.autoFork();
+
+The options argument can contain 3 different properties.
+
+- `exec` are the file path to the worker file, by default this is the same file as the master.
+- `args` are a array of arguments send along with the worker, by default this is `process.argv.slice(2)`.
+- `silent`, if this option is true the output of a worker won't propagate to the master, by default this is false.
+
+### cluster.settings
+
+All settings set by the `.setupMaster` is stored in this settings object.
+This object is not supposed to be change or set manually, by you.
+
+All propertys are `undefined` if they are not yet set.
+
 ### cluster.fork([env])
 
 Spawn a new worker process. This can only be called from the master process.
index 6f003f5212076be093b0bb87dd1e212964b77fdf..ce593ad01f3a63763a4f3f1310a12e12c2b212bd 100644 (file)
@@ -61,8 +61,6 @@ var cluster = module.exports = new cluster();
 var masterStarted = false;
 var ids = 0;
 var serverHandlers = {};
-var workerFilename;
-var workerArgs;
 
 // Used in the worker:
 var serverLisenters = {};
@@ -78,6 +76,9 @@ cluster.worker = cluster.isWorker ? {} : null;
 // The workers array is oly used in the naster
 cluster.workers = cluster.isMaster ? {} : null;
 
+// Settings object
+var settings = cluster.settings = {};
+
 // Simple function there call a function on each worker
 function eachWorker(cb) {
   // Go througe all workers
@@ -88,36 +89,44 @@ function eachWorker(cb) {
   }
 }
 
-// Call this from the master process. It will start child workers.
-//
-// options.workerFilename
-// Specifies the script to execute for the child processes. Default is
-// process.argv[1]
-//
-// options.args
-// Specifies program arguments for the workers. The Default is
-// process.argv.slice(2)
-//
-// options.workers
-// The number of workers to start. Defaults to os.cpus().length.
-function startMaster() {
-  // This can only be called from the master.
-  assert(cluster.isMaster);
+cluster.setupMaster = function(options) {
+   // This can only be called from the master.
+   assert(cluster.isMaster);
 
-  if (masterStarted) return;
-  masterStarted = true;
+  // Don't allow this function to run more that once
+   if (masterStarted) return;
+   masterStarted = true;
 
-  workerFilename = process.argv[1];
-  workerArgs = process.argv.slice(2);
+  // Get filename and arguments
+  options = options || {};
+
+  // Set settings object
+  settings = cluster.settings = {
+    exec: options.exec || process.argv[1],
+    args: options.args || process.argv.slice(2),
+    silent: options.silent || false
+  };
 
-  process.on('uncaughtException', function(e) {
-    console.error('Exception in cluster master process: ' +
-        e.message + '\n' + e.stack);
+  // Kill workers when a uncaught exception is received
+  process.on('uncaughtException', function(err) {
+    // Did the user install a listener? If so, it overrides this one.
+    if (process.listeners('uncaughtException').length > 1) return;
 
+    // Output the error stack, and create on if non exist
+    if (!(err instanceof Error)) {
+      err = new Error(err);
+    }
+    console.error(err.stack);
+
+    // quick destroy cluster
     quickDestroyCluster();
+    // when done exit process with error code: 1
     process.exit(1);
-  });
-}
+ });
+
+  // emit setup event
+  cluster.emit('setup');
+};
 
 // Check if a message is internal only
 var INTERNAL_PREFIX = 'NODE_CLUSTER_';
@@ -275,10 +284,10 @@ function Worker(customEnv) {
     }
 
     // fork worker
-    this.process = fork(workerFilename, workerArgs, {
-      'env': envCopy
+    this.process = fork(settings.exec, settings.args, {
+      'env': envCopy,
+      'silent': settings.silent
     });
-
   } else {
     this.process = process;
   }
@@ -426,7 +435,7 @@ cluster.fork = function(env) {
   assert(cluster.isMaster);
 
   // Make sure that the master has been initalized
-  startMaster();
+  cluster.setupMaster();
 
   return (new cluster.Worker(env));
 };
diff --git a/test/simple/test-cluster-kill-workers.js b/test/simple/test-cluster-kill-workers.js
deleted file mode 100644 (file)
index 526869c..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-// 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.
-
-// This test checks that if we kill a cluster master immediately after fork,
-// before the worker has time to register itself, that the master will still
-// clean up the worker.
-// https://github.com/joyent/node/issues/2047
-
-var common = require('../common');
-var assert = require('assert');
-var cluster = require('cluster');
-var fork = require('child_process').fork;
-
-var isTestRunner = process.argv[2] != 'child';
-
-if (isTestRunner) {
-  console.log("starting master...");
-  var master = fork(__filename, [ 'child' ]);
-
-  console.log("master pid =", master.pid);
-
-  var workerPID;
-
-  master.on("message", function(m) {
-    console.log("got message from master:", m);
-    if (m.workerPID) {
-      console.log("worker pid =", m.workerPID);
-      workerPID = m.workerPID;
-    }
-  });
-
-  var gotExit = false;
-  var gotKillException = false;
-
-  master.on('exit', function(code) {
-    gotExit = true;
-    assert(code != 0);
-    assert(workerPID > 0);
-    try {
-      process.kill(workerPID, 0);
-    } catch(e) {
-      // workerPID is no longer running
-      console.log(e)
-      assert(e.code == 'ESRCH');
-      gotKillException = true;
-    }
-  })
-
-  process.on('exit', function() {
-    assert(gotExit);
-    assert(gotKillException);
-  });
-} else {
-  // Cluster stuff.
-  if (cluster.isMaster) {
-    var worker = cluster.fork();
-    process.send({ workerPID: worker.process.pid });
-    // should kill the worker too
-    throw new Error('kill master');
-  } else {
-    setTimeout(function() {
-      assert(false, 'worker should have been killed');
-    }, 2500);
-  }
-}
-
diff --git a/test/simple/test-cluster-master-error.js b/test/simple/test-cluster-master-error.js
new file mode 100644 (file)
index 0000000..4add0eb
--- /dev/null
@@ -0,0 +1,132 @@
+// 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.
+
+
+var common = require('../common');
+var assert = require('assert');
+var cluster = require('cluster');
+
+// Cluster setup
+if (cluster.isWorker) {
+  var http = require('http');
+  http.Server(function() {
+
+  }).listen(common.PORT, '127.0.0.1');
+
+} else if (process.argv[2] === 'cluster') {
+
+  var totalWorkers = 2;
+
+  // Send PID to testcase process
+  var forkNum = 0;
+  cluster.on('fork', function forkEvent(worker) {
+
+    // Send PID
+    process.send({
+      cmd: 'worker',
+      workerPID: worker.process.pid
+    });
+
+    // Stop listening when done
+    if (++forkNum === totalWorkers) {
+      cluster.removeListener('fork', forkEvent);
+    }
+  });
+
+  // Throw accidently error when all workers are listening
+  var listeningNum = 0;
+  cluster.on('listening', function listeningEvent() {
+
+    // When all workers are listening
+    if (++listeningNum === totalWorkers) {
+      // Stop listening
+      cluster.removeListener('listening', listeningEvent);
+
+      // throw accidently error
+      process.nextTick(function() {
+        throw 'accidently error';
+      });
+    }
+
+  });
+
+  // Startup a basic cluster
+  cluster.fork();
+  cluster.fork();
+
+} else {
+  // This is the testcase
+
+  var fork = require('child_process').fork;
+
+  var isAlive = function(pid) {
+    try {
+      //this will throw an error if the process is dead
+      process.kill(pid, 0);
+
+      return true;
+    } catch (e) {
+      return false;
+    }
+  };
+
+  var existMaster = false;
+  var existWorker = false;
+
+  // List all workers
+  var workers = [];
+
+  // Spawn a cluster process
+  var master = fork(process.argv[1], ['cluster'], {silent: true});
+
+  // Handle messages from the cluster
+  master.on('message', function(data) {
+
+    // Add worker pid to list and progress tracker
+    if (data.cmd === 'worker') {
+      workers.push(data.workerPID);
+    }
+  });
+
+  // When cluster is dead
+  master.on('exit', function(code) {
+
+    // Check that the cluster died accidently
+    existMaster = (code === 1);
+
+    // When master is dead all workers should be dead to
+    var alive = false;
+    workers.forEach(function(pid) {
+      if (isAlive(pid)) {
+        alive = true;
+      }
+    });
+
+    // If a worker was alive this did not act as expected
+    existWorker = !alive;
+  });
+
+  process.once('exit', function() {
+    assert.ok(existMaster, 'The master did not die after an error was throwed');
+    assert.ok(existWorker, 'The workers did not die after an error in the master');
+  });
+
+}
diff --git a/test/simple/test-cluster-setup-master.js b/test/simple/test-cluster-setup-master.js
new file mode 100644 (file)
index 0000000..7f542e4
--- /dev/null
@@ -0,0 +1,89 @@
+// 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.
+
+
+var common = require('../common');
+var assert = require('assert');
+var cluster = require('cluster');
+
+if (cluster.isWorker) {
+
+  // Just keep the worker alive
+  process.send(process.argv[2]);
+
+} else if (cluster.isMaster) {
+
+  var checks = {
+    args: false,
+    setupEvent: false,
+    settingsObject: false
+  };
+
+  var totalWorkers = 2;
+
+  cluster.once('setup', function() {
+    checks.setupEvent = true;
+
+    var settings = cluster.settings;
+    if (settings &&
+        settings.args && settings.args[0] === 'custom argument' &&
+        settings.silent === true &&
+        settings.exec === process.argv[1]) {
+      checks.settingsObject = true;
+    }
+  });
+
+  // Setup master
+  cluster.setupMaster({
+    args: ['custom argument'],
+    silent: true
+  });
+
+  var correctIn = 0;
+
+  cluster.on('online', function lisenter(worker) {
+
+    worker.once('message', function(data) {
+      correctIn += (data === 'custom argument' ? 1 : 0);
+      if (correctIn === totalWorkers) {
+        checks.args = true;
+      }
+      worker.destroy();
+    });
+
+    // All workers are online
+    if (cluster.onlineWorkers === totalWorkers) {
+      checks.workers = true;
+    }
+  });
+
+  // Start all workers
+  cluster.fork();
+  cluster.fork();
+
+  // Check all values
+  process.once('exit', function() {
+    assert.ok(checks.args, 'The arguments was noy send to the worker');
+    assert.ok(checks.setupEvent, 'The setup event was never emitted');
+    assert.ok(checks.settingsObject, 'The settingsObject do not have correct properties');
+  });
+
+}