From f9a47debfca854666e45de299b0bb630db4a1548 Mon Sep 17 00:00:00 2001 From: Andreas Madsen Date: Thu, 5 Jan 2012 20:09:43 +0100 Subject: [PATCH] Add cluster.setupMaster Fixes #2470 --- doc/api/cluster.markdown | 34 ++++++ lib/cluster.js | 69 ++++++------ test/simple/test-cluster-kill-workers.js | 84 --------------- test/simple/test-cluster-master-error.js | 132 +++++++++++++++++++++++ test/simple/test-cluster-setup-master.js | 89 +++++++++++++++ 5 files changed, 294 insertions(+), 114 deletions(-) delete mode 100644 test/simple/test-cluster-kill-workers.js create mode 100644 test/simple/test-cluster-master-error.js create mode 100644 test/simple/test-cluster-setup-master.js diff --git a/doc/api/cluster.markdown b/doc/api/cluster.markdown index 96e4ef8f37a9cb..d5916e6028c8ce 100644 --- a/doc/api/cluster.markdown +++ b/doc/api/cluster.markdown @@ -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. diff --git a/lib/cluster.js b/lib/cluster.js index 6f003f5212076b..ce593ad01f3a63 100644 --- a/lib/cluster.js +++ b/lib/cluster.js @@ -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 index 526869c8caeb05..00000000000000 --- a/test/simple/test-cluster-kill-workers.js +++ /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 index 00000000000000..4add0eba1322c4 --- /dev/null +++ b/test/simple/test-cluster-master-error.js @@ -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 index 00000000000000..7f542e4e68a15b --- /dev/null +++ b/test/simple/test-cluster-setup-master.js @@ -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'); + }); + +}