cluster-service
Advanced tools
Comparing version 1.0.5 to 2.0.0-alpha1
@@ -0,1 +1,14 @@ | ||
## v2.0.0 - TBD | ||
Features: | ||
* Proxy support with dynamic app versioning | ||
* Worker process downgrade from root via `workerGid` & `workerUid` | ||
Enhancements: | ||
* Refactored communication between processes | ||
* Worker processes may now trigger commands (same) and wait for response (new) | ||
## v1.0.0 - 5/8/2014 | ||
@@ -19,3 +32,3 @@ | ||
* #57. Cannot use '--run' and '--config' together | ||
* #58. Custom events from master & workers | ||
* #58. Custom events from master & workers | ||
@@ -22,0 +35,0 @@ |
@@ -13,2 +13,3 @@ var cluster = require("cluster"); | ||
exports.results = require("./lib/util").results; | ||
exports.msgBus = require("./lib/message-bus"); | ||
@@ -60,2 +61,3 @@ exports.workerReady = require("./lib/worker-ready"); | ||
exports.netStats = require("./lib/net-stats"); | ||
exports.proxy = require('./lib/proxy'); | ||
@@ -68,6 +70,12 @@ if ( | ||
cluster.worker.module = {}; | ||
cluster.worker.env = process.env; | ||
var workers = require("./lib/workers"); | ||
workers.demote(); | ||
// load the worker if not already loaded | ||
// async, in case worker loads cluster-service, we need to return before | ||
// async, in case worker loads cluster-service, we need to return before | ||
// it's avail | ||
setTimeout(function() { | ||
setImmediate(function() { | ||
cluster.worker.module = require(process.env.worker); | ||
@@ -82,6 +90,6 @@ if (global.cservice.locals.workerReady === undefined | ||
} | ||
}, 10); | ||
}); | ||
// start worker monitor to establish two-way relationship with master | ||
require("./lib/workers").monitor(); | ||
workers.monitor(); | ||
} |
var http = require('http'); | ||
var cservice = require("cluster-service"); | ||
var cservice = require("../cluster-service"); | ||
@@ -4,0 +4,0 @@ cservice.workerReady(false); // inform cservice we're not ready yet |
@@ -70,11 +70,3 @@ var async = require("async"), | ||
return function(cb) { | ||
// kill new worker if takes too long | ||
var newKiller = null; | ||
var newWorker = null; | ||
var exitListener = function() { | ||
if (newKiller) { | ||
clearTimeout(newKiller); | ||
} | ||
}; | ||
var w; | ||
var pendingWorker = null; | ||
@@ -89,11 +81,14 @@ if (worker.cservice.restart === false && explicitRestart === false) { | ||
// kill new worker if takes too long | ||
var newWorkerTimeout = null; | ||
var isNewWorkerTerminated = false; | ||
if (options.timeout > 0) { // start timeout if specified | ||
newKiller = setTimeout(function() { | ||
w = newWorker; | ||
newWorker = null; | ||
if (w) { | ||
w.removeListener("exit", exitListener); // remove temp listener | ||
w.kill("SIGKILL"); // go get'em, killer | ||
newWorkerTimeout = setTimeout(function() { | ||
if (pendingWorker) { | ||
isNewWorkerTerminated = true; | ||
pendingWorker.on('exit', function () { | ||
cb("timed out"); | ||
}); | ||
pendingWorker.kill("SIGKILL"); // go get'em, killer | ||
} | ||
cb("timed out"); | ||
}, options.timeout); | ||
@@ -103,14 +98,13 @@ } | ||
// lets start new worker | ||
newWorker = evt.service.newWorker(worker.cservice, function(err, newWorker){ | ||
var killer; | ||
newWorker.removeListener("exit", exitListener); // remove temp listener | ||
newWorker = null; | ||
if (newKiller) { // timeout no longer needed | ||
clearTimeout(newKiller); | ||
pendingWorker = evt.service.newWorker(worker.cservice, function(err) { | ||
pendingWorker = null; | ||
if (newWorkerTimeout) { // timeout no longer needed | ||
clearTimeout(newWorkerTimeout); | ||
} | ||
if (isNewWorkerTerminated) return; | ||
// ok, lets stop old worker | ||
killer = null; | ||
var oldWorkerTimeout = null; | ||
if (options.timeout > 0) { // start timeout if specified | ||
killer = setTimeout(function() { | ||
oldWorkerTimeout = setTimeout(function() { | ||
worker.kill("SIGKILL"); // go get'em, killer | ||
@@ -121,8 +115,8 @@ }, options.timeout); | ||
worker.on("exit", function() { | ||
if (killer) { | ||
clearTimeout(killer); | ||
if (oldWorkerTimeout) { | ||
clearTimeout(oldWorkerTimeout); | ||
} | ||
// exit complete, fire callback | ||
setTimeout(cb, 100); // slight delay in case other events are piled up | ||
setImmediate(cb); // slight delay in case other events are piled up | ||
}); | ||
@@ -129,0 +123,0 @@ |
@@ -31,26 +31,19 @@ /* jshint loopfunc:true */ | ||
worker.process.on( | ||
"exit", | ||
getExitHandler( | ||
evt | ||
, worker | ||
, options.timeout > 0 | ||
? setTimeout(getKiller(worker), options.timeout) | ||
: null | ||
, function() { | ||
workersToKill--; | ||
if (workersToKill === 0) { | ||
// no workers remain | ||
if (evt.service.workers.length === 0) { | ||
evt.locals.reason = "kill"; | ||
cservice.log("All workers shutdown. Exiting...".warn); | ||
evt.service.stop(options.timeout, cb); | ||
} else { | ||
cb(null, "Worker shutdown"); // DONE | ||
} | ||
} | ||
var killTimeout = options.timeout > 0 | ||
? setTimeout(getKiller(worker), options.timeout) | ||
: null; | ||
worker.on("exit", getExitHandler(evt, worker, killTimeout, function() { | ||
workersToKill--; | ||
if (workersToKill === 0) { | ||
// no workers remain | ||
if (evt.service.workers.length === 0) { | ||
evt.locals.reason = "kill"; | ||
cservice.log("All workers shutdown. Exiting...".warn); | ||
evt.service.stop(options.timeout, cb); | ||
} else { | ||
cb(null, "Worker shutdown"); // DONE | ||
} | ||
) | ||
); | ||
} | ||
})); | ||
require("../workers").exitGracefully(worker); | ||
@@ -108,3 +101,2 @@ }); | ||
clearTimeout(killer); | ||
killer = null; | ||
} | ||
@@ -111,0 +103,0 @@ |
@@ -67,36 +67,33 @@ var async = require("async"), | ||
return function(cb) { | ||
var pendingWorker; | ||
// kill new worker if takes too long | ||
var newKiller = null; | ||
var newWorker; | ||
var exitListener = function() { | ||
if (newKiller) { | ||
clearTimeout(newKiller); | ||
} | ||
}; | ||
var startTimeout = null; | ||
var isWorkerTerminated = false; | ||
if (options.timeout > 0) { // start timeout if specified | ||
newKiller = setTimeout(function() { | ||
if (!newWorker) | ||
startTimeout = setTimeout(function() { | ||
if (!pendingWorker) | ||
return; | ||
newWorker.removeListener("exit", exitListener); // remove temp listener | ||
newWorker.kill("SIGKILL"); // go get'em, killer | ||
cb("timed out"); | ||
isWorkerTerminated = true; | ||
pendingWorker.on('exit', function () { | ||
cb("timed out"); | ||
}); | ||
pendingWorker.kill("SIGKILL"); // go get'em, killer | ||
}, options.timeout); | ||
newKiller.unref(); | ||
startTimeout.unref(); | ||
} | ||
// lets start new worker | ||
newWorker = evt.service.newWorker(options, function(err, newWorker) { | ||
if (newWorker) { // won't exist if failure | ||
newWorker.removeListener("exit", exitListener); // remove temp listener | ||
pendingWorker = evt.service.newWorker(options, function(err) { | ||
pendingWorker = null; | ||
if (startTimeout) { // timeout no longer needed | ||
clearTimeout(startTimeout); | ||
} | ||
if (newKiller) { // timeout no longer needed | ||
clearTimeout(newKiller); | ||
newKiller = null; | ||
if (!isWorkerTerminated) { | ||
cb(err); | ||
} | ||
cb(err); | ||
}); | ||
}; | ||
} |
var async = require("async"), | ||
util = require("util"), | ||
extend = require("extend"), | ||
cservice = require("../../cluster-service"); | ||
@@ -32,3 +31,3 @@ | ||
// use original worker options as default, by overwrite using new options | ||
workerOptions = extend(true, {}, worker.cservice, options); | ||
workerOptions = util._extend({}, worker.cservice, options); | ||
@@ -82,17 +81,16 @@ tasks.push(getTask(evt, worker, workerOptions)); | ||
var pendingWorker; | ||
// kill new worker if takes too long | ||
var newKiller = null; | ||
var newWorker; | ||
var exitListener = function() { | ||
if (newKiller) { | ||
clearTimeout(newKiller); | ||
} | ||
}; | ||
var killer; | ||
var newWorkerTimeout = null; | ||
var isNewWorkerTerminated = false; | ||
if (options.timeout > 0) { // start timeout if specified | ||
newWorkerTimeout = setTimeout(function() { | ||
if (!pendingWorker) return; | ||
if (options.timeout > 0) { // start timeout if specified | ||
newKiller = setTimeout(function() { | ||
newWorker.removeListener("exit", exitListener);// remove temp listener | ||
newWorker.kill("SIGKILL"); // go get'em, killer | ||
cb("timed out"); | ||
isNewWorkerTerminated = true; | ||
pendingWorker.on('exit', function () { | ||
cb("timed out"); | ||
}); | ||
pendingWorker.kill("SIGKILL"); // go get'em, killer | ||
}, options.timeout); | ||
@@ -102,9 +100,8 @@ } | ||
// lets start new worker | ||
newWorker = evt.service.newWorker(options, function(err, newWorker) { | ||
if (newWorker) { // won't exist if failure | ||
newWorker.removeListener("exit", exitListener); // remove temp listener | ||
pendingWorker = evt.service.newWorker(options, function (err) { | ||
pendingWorker = null; | ||
if (newWorkerTimeout) { // timeout no longer needed | ||
clearTimeout(newWorkerTimeout); | ||
} | ||
if (newKiller) { // timeout no longer needed | ||
clearTimeout(newKiller); | ||
} | ||
if (err) { | ||
@@ -116,5 +113,5 @@ cb(err); | ||
// ok, lets stop old worker | ||
killer = null; | ||
var oldWorkerTimeout = null; | ||
if (options.timeout > 0) { // start timeout if specified | ||
killer = setTimeout(function() { | ||
oldWorkerTimeout = setTimeout(function() { | ||
worker.kill("SIGKILL"); // go get'em, killer | ||
@@ -125,8 +122,8 @@ }, options.timeout); | ||
worker.on("exit", function() { | ||
if (killer) { | ||
clearTimeout(killer); | ||
if (oldWorkerTimeout) { | ||
clearTimeout(oldWorkerTimeout); | ||
} | ||
// exit complete, fire callback | ||
setTimeout(cb, 250); // slight delay in case other events are piled up | ||
setImmediate(cb); // slight delay in case other events are piled up | ||
}); | ||
@@ -133,0 +130,0 @@ |
@@ -11,3 +11,4 @@ var path = require("path"), | ||
+ ") exited, reason: " | ||
+ (reason || cservice.locals.reason || "Unknown")).warn | ||
+ (reason || worker.cservice.reason || | ||
cservice.locals.reason || "Unknown")).warn | ||
); | ||
@@ -14,0 +15,0 @@ cb(); |
@@ -1,3 +0,3 @@ | ||
var async = require("async"), | ||
extend = require("extend"); | ||
var async = require("async"); | ||
var cservice = require("../../cluster-service"); | ||
@@ -47,7 +47,7 @@ module.exports = function(evt, cb, cmd) { | ||
msgCb = function (msg) { | ||
if (msg && msg.processDetails) { | ||
processDetails = msg.processDetails; | ||
if (msg && msg.cservice.processDetails) { | ||
processDetails = msg.cservice.processDetails; | ||
} | ||
if (msg && msg.netStats) { | ||
netStats = msg.netStats; | ||
if (msg && msg.cservice.netStats) { | ||
netStats = msg.cservice.netStats; | ||
} | ||
@@ -73,4 +73,4 @@ if (processDetails && netStats) { | ||
worker.on("message", msgCb); | ||
worker.process.send({cservice: "processDetails"}); | ||
worker.process.send({cservice: "netStats"}); | ||
worker.process.send(cservice.msgBus.createMessage("processDetails")); | ||
worker.process.send(cservice.msgBus.createMessage("netStats")); | ||
}; | ||
@@ -77,0 +77,0 @@ } |
@@ -7,2 +7,3 @@ var os = require("os"); | ||
workers: {}, | ||
workerProcesses: {}, | ||
state: 0, // 0-not running, 1-starting, 2-running | ||
@@ -14,2 +15,40 @@ isBusy: false, | ||
net: { servers: {} }, | ||
proxy: { | ||
configPath: undefined, | ||
versionPath: undefined, | ||
options: { | ||
versionPath: undefined, | ||
versionHeader: "x-version", | ||
workerFilename: "worker.js", | ||
versionPorts: "11000-12000", | ||
nonDefaultWorkerCount: 1, | ||
nonDefaultWorkerIdleTime: 3600, | ||
bindings: [ | ||
/* | ||
{ | ||
port: 80, | ||
workerCount: 2, | ||
redirect: 443 | ||
}, | ||
{ | ||
port: 443, | ||
workerCount: 2, | ||
tlsOptions: { | ||
key: '/my/cert.key', | ||
cert: '/my/cert.crt' | ||
} | ||
} | ||
*/ | ||
] | ||
}, | ||
versions: { | ||
/* | ||
'versionStr': { | ||
port: 7112, | ||
isDefault: false, | ||
online: false | ||
} | ||
*/ | ||
} | ||
}, | ||
options: { | ||
@@ -33,2 +72,5 @@ host: "localhost", | ||
commands: undefined, | ||
proxy: undefined, | ||
workerGid: undefined, | ||
workerUid: undefined, | ||
colors: { | ||
@@ -35,0 +77,0 @@ cservice: "grey", |
@@ -55,6 +55,27 @@ /* jshint loopfunc:true */ | ||
cluster.on("exit", function(worker, code, signal) { | ||
// stop tracking | ||
var version = cservice.locals.proxy.versions[worker.cservice.version]; | ||
if (version) { | ||
// get all proxy workers for a specific version | ||
var versionWorkers = | ||
cservice.proxy.getVersionWorkers(worker.cservice.version); | ||
// exclude our exiting worker process in case it's still returned | ||
versionWorkers = versionWorkers.filter(function(versionWorker) { | ||
return worker.process.pid !== versionWorker.process.pid; | ||
}); | ||
if (versionWorkers.length === 0) { | ||
// if no workers remain for a given version, drop the version | ||
delete cservice.locals.proxy.versions[worker.cservice.version]; | ||
// inform proxy workers of version change | ||
cservice.proxy.updateProxyWorkers(); | ||
} | ||
} | ||
delete cservice.locals.workerProcesses[worker.process.pid]; | ||
cservice.trigger("workerExit", worker); | ||
// do not restart if there is a reason, or disabled | ||
if ( | ||
typeof (cservice.locals.reason) === "undefined" | ||
!(cservice.locals.reason || worker.cservice.reason) | ||
&& worker.suicide !== true | ||
@@ -79,9 +100,11 @@ && cservice.locals.restartOnFailure === true | ||
cservice.locals.state = 2; // running | ||
cservice.proxy.start({}, function() { | ||
cservice.locals.state = 2; // running | ||
// now that listener is ready, process queued start requests | ||
for (i = 0; i < startRequests.length; i++) { | ||
startRequests[i](); // execute | ||
} | ||
startRequests = []; | ||
// now that listener is ready, process queued start requests | ||
for (i = 0; i < startRequests.length; i++) { | ||
startRequests[i](); // execute | ||
} | ||
startRequests = []; | ||
}); | ||
}); | ||
@@ -97,3 +120,3 @@ } else if (cservice.locals.state === 1) { // if still starting, queue requests | ||
workersForked = 0; | ||
if (options.workers !== null) { | ||
@@ -126,3 +149,3 @@ workers = typeof options.workers === "string" | ||
} | ||
// if no forking took place, make sure cb is invoked | ||
@@ -158,3 +181,3 @@ if (workersForked === 0) { | ||
} | ||
httpserver.init(options, function(err) { | ||
@@ -161,0 +184,0 @@ if (!err) { |
@@ -22,3 +22,3 @@ var cservice = require("../cluster-service"); | ||
} | ||
for (var i = 0; i < servers.length; i++) { | ||
@@ -28,3 +28,3 @@ var server = servers[i]; | ||
continue; // ignore if already added | ||
server.cservice = { | ||
@@ -35,3 +35,3 @@ id: Math.random().toString(), // track by id | ||
cservice.locals.net.servers[server.cservice.id] = server; | ||
listenToNetServer(server); | ||
@@ -55,3 +55,3 @@ netStats(server); | ||
delete server.cservice; | ||
stopListeningToNetServer(server); | ||
@@ -63,3 +63,3 @@ } | ||
var tasks = []; | ||
for (var id in cservice.locals.net.servers) { | ||
@@ -69,10 +69,10 @@ var server = cservice.locals.net.servers[id]; | ||
continue; | ||
tasks.push(createWaitForReadyTask(server)); | ||
} | ||
if (tasks.length === 0) { | ||
return cb(); | ||
} | ||
async.parallel(tasks, cb); | ||
@@ -107,3 +107,3 @@ } | ||
} | ||
if (tasks.length === 0) { | ||
@@ -118,16 +118,3 @@ return cb(); | ||
return function(cb) { | ||
var tmpRequestCb = function(req, res) { | ||
if (res.headersSent === false) { | ||
// required to gracefully close connections on pending requests | ||
// this logic is typically only hit under VERY high load | ||
res.setHeader("Connection", "close"); | ||
} | ||
}; | ||
var tmpCloseCb = function() { | ||
server.removeListener("close", tmpCloseCb); | ||
//server.removeListener("request", tmpRequestCb); | ||
cb(null, true); | ||
}; | ||
server.on("close", tmpCloseCb); | ||
//server.on("request", tmpRequestCb); | ||
server.once("close", function() { cb(null, true); }); | ||
server.close(); | ||
@@ -141,3 +128,3 @@ }; | ||
serverListenOld = net.Server.prototype.listen; | ||
net.Server.prototype.listen = serverListenNew; | ||
@@ -160,3 +147,3 @@ } | ||
this.on("listening", serverOnListening); // ready on event | ||
return serverListenOld.apply(this, arguments); // call original listen | ||
@@ -163,0 +150,0 @@ } |
@@ -24,3 +24,3 @@ var cservice = require("../cluster-service"); | ||
} | ||
server.on("connection", function(connection) { | ||
@@ -52,12 +52,11 @@ net.connections++; | ||
process.on("message", function(msg) { | ||
if (!msg || typeof msg.cservice !== "string") { | ||
if (!cservice.msgBus.isValidMessage(msg)) { | ||
return; // ignore | ||
} | ||
switch (msg.cservice) { | ||
switch (msg.cservice.cmd) { | ||
case "netStats": | ||
process.send({ | ||
cservice: "netStats", | ||
process.send(cservice.msgBus.createMessage("netStats", { | ||
netStats: net | ||
}); | ||
})); | ||
break; | ||
@@ -82,3 +81,3 @@ } | ||
; | ||
// reset | ||
@@ -85,0 +84,0 @@ stats.lastCheck = now; |
@@ -5,3 +5,3 @@ var cservice = require("../cluster-service"), | ||
fs = require("fs"), | ||
extend = require("extend"); | ||
util = require("util"); | ||
@@ -12,9 +12,11 @@ module.exports = exports = newWorker; | ||
var worker; | ||
options = extend(true, {}, { | ||
options = util._extend(util._extend({}, { | ||
worker: "./worker.js", | ||
count: undefined, | ||
restart: true, | ||
type: 'user', | ||
version: undefined, | ||
cwd: undefined, | ||
onStop: false | ||
}, options); | ||
}), options); | ||
options.ready = false; | ||
@@ -44,2 +46,17 @@ if ( | ||
options.onReady = cb; | ||
var version; | ||
if (options.version) { | ||
// track workers with version | ||
version = cservice.locals.proxy.versions[options.version]; | ||
if (!version) { | ||
version = { | ||
name: options.version, | ||
port: options.PROXY_PORT, | ||
online: false | ||
}; | ||
cservice.locals.proxy.versions[options.version] = version; | ||
} | ||
} | ||
worker = cluster.fork(options); | ||
@@ -49,2 +66,5 @@ worker.cservice = options; | ||
// track every worker by pid | ||
cservice.locals.workerProcesses[worker.process.pid] = worker; | ||
return worker; | ||
@@ -55,3 +75,3 @@ } | ||
var worker = this; | ||
if (!msg || !msg.cservice || !msg.cservice.cmd) { | ||
if (!cservice.msgBus.isValidMessage(msg)) { | ||
return; // ignore invalid cluster-service messages | ||
@@ -64,2 +84,10 @@ } | ||
case "workerReady": | ||
var version = cservice.locals.proxy.versions[worker.cservice.version]; | ||
if (version) { | ||
// if version detected within worker, flag as online | ||
version.online = true; | ||
// notify proxy workers of version update | ||
cservice.proxy.updateProxyWorkers(); | ||
} | ||
if (worker.cservice.ready === false) { | ||
@@ -77,2 +105,10 @@ // preserve preference between restarts, etc | ||
if (args && args.length > 0) { | ||
if (msg.cservice.cb === true) { | ||
args.splice(1, 0, function(err, result) { | ||
// forward response to worker that requested the trigger | ||
cservice.msgBus.respondToMessage(msg, worker.process, err, result); | ||
}); | ||
} else { | ||
args.splice(1, 0, null); // no callback necessary | ||
} | ||
cservice.trigger.apply(cservice, args); | ||
@@ -79,0 +115,0 @@ } |
@@ -6,3 +6,3 @@ var cservice = require("../cluster-service"), | ||
colors = require("colors"), | ||
extend = require("extend"); | ||
util = require("util"); | ||
@@ -16,3 +16,3 @@ module.exports = exports = start; | ||
if (cluster.isWorker === true) { | ||
// ignore starts if not master. do NOT invoke masterCb, as that is | ||
// ignore starts if not master. do NOT invoke masterCb, as that is | ||
// reserved for master callback | ||
@@ -38,5 +38,5 @@ | ||
var fileOptions = JSON.parse(fs.readFileSync(options.config)); | ||
options = extend(true, fileOptions, options); | ||
options = util._extend(fileOptions, options); | ||
} | ||
cservice.locals.options = extend(true, cservice.locals.options, options); | ||
cservice.locals.options = util._extend(cservice.locals.options, options); | ||
if ("workers" in options) { // overwrite workers if provided | ||
@@ -66,3 +66,3 @@ cservice.locals.options.workers = options.workers; | ||
} | ||
process.exit(1); // graceful exit | ||
process.exit(0); // graceful exit | ||
}); | ||
@@ -78,3 +78,3 @@ } else { | ||
cservice.log("Startup failed, exiting...".warn); | ||
process.exit(1); // graceful exit | ||
process.exit(0); // graceful exit | ||
} | ||
@@ -98,2 +98,2 @@ } | ||
} | ||
} | ||
} |
@@ -13,16 +13,16 @@ var cservice = require("../cluster-service"); | ||
cservice.trigger("shutdown", function() { | ||
if (cb) cb(null, "Shutting down..."); | ||
require("./http-server").close(); | ||
if (cservice.options.cli === true) { | ||
process.exit(1); | ||
} | ||
handleWorkersExited(cb); | ||
}, "all", timeout); | ||
} else { // gracefully shutdown | ||
if (cb) cb(null, "Shutting down..."); | ||
require("./http-server").close(); | ||
cservice.locals.state = 0; | ||
if (cservice.options.cli === true) { | ||
process.exit(1); | ||
} | ||
handleWorkersExited(cb); | ||
} | ||
} | ||
function handleWorkersExited(cb) { | ||
if (cb) cb(null, "Shutting down..."); | ||
require("./http-server").close(); | ||
cservice.locals.state = 0; | ||
if (cservice.options.cli === true) { | ||
process.exit(1); | ||
} | ||
} |
@@ -6,7 +6,10 @@ var cservice = require("../cluster-service"); | ||
function trigger(eventName, cb) { | ||
var args = Array.prototype.slice.call(arguments); | ||
if (cservice.isWorker === true) { | ||
process.send({ | ||
cservice: { | ||
cmd: "trigger", | ||
args: Array.prototype.slice.call(arguments) | ||
args.splice(1, 1); // remove cb from args if it exists | ||
cservice.msgBus.sendMessage("trigger", { args: args, cb: true }, | ||
null, function(err, result) { | ||
// wait for response from master | ||
if (typeof cb === "function") { | ||
cb(err, result); | ||
} | ||
@@ -17,8 +20,7 @@ }); | ||
var evt = cservice.locals.events[eventName]; | ||
var args; | ||
var i; | ||
if (!evt) { | ||
// invoke callback if provided instead of throwing | ||
if (typeof arguments[1] === "function") { | ||
arguments[1]("Event " + eventName + " not found"); | ||
if (typeof cb === "function") { | ||
cb("Event " + eventName + " not found"); | ||
} else { | ||
@@ -29,10 +31,5 @@ throw new Error("Event " + eventName + " not found"); | ||
args = [evt]; // event is always first arg | ||
if (arguments.length > 1) { // grab custom args | ||
for (i = 1; i < arguments.length; i++) { | ||
args.push(arguments[i]); | ||
} | ||
} | ||
args.splice(0, 1, evt); | ||
if (args.length < 2 || typeof args[1] !== "function") { | ||
if (typeof cb !== "function") { | ||
// auto-inject dummy callback if not provided | ||
@@ -44,5 +41,4 @@ args.splice(1, 0, function DummyCallback(err, results) { | ||
//exports.log("trigger." + eventName + ".args=" + args.length); | ||
// invoke event callback | ||
return evt.cb.apply(null, args); | ||
} |
var cservice = require("../cluster-service"), | ||
cluster = require("cluster"), | ||
util = require("util"), | ||
messageBus = require("./message-bus"), | ||
onWorkerStop = null; | ||
@@ -30,3 +31,3 @@ | ||
} | ||
onWorkerStop = options.onWorkerStop; | ||
@@ -36,14 +37,14 @@ | ||
// allow worker to inform the master when ready to speed up initialization | ||
process.send({ | ||
cservice: { | ||
cmd: "workerReady", | ||
// allow worker to inform the master when ready to speed up initialization | ||
process.send( | ||
messageBus.createMessage("workerReady", { | ||
onStop: (typeof options.onWorkerStop === "function") | ||
} | ||
}); | ||
}) | ||
); | ||
} | ||
function onMessageFromMaster(msg) { | ||
if (!msg || !msg.cservice || !msg.cservice.cmd) { | ||
return; // ignore invalid cluster-service messages | ||
if (!messageBus.isValidMessage(msg) || | ||
cservice.msgBus.processMessage(msg)) { | ||
return; | ||
} | ||
@@ -50,0 +51,0 @@ |
@@ -7,3 +7,5 @@ var cservice = require("../cluster-service"), | ||
exports.getByPID = getByPID; | ||
exports.getByPIDFromCache = getByPIDFromCache; | ||
exports.exitGracefully = exitGracefully; | ||
exports.demote = demote; | ||
@@ -17,7 +19,11 @@ function get() { | ||
worker = cworkers[k]; | ||
worker.pid = worker.process.pid; | ||
workers.push(worker); | ||
if ((!worker.isDead || !worker.isDead()) | ||
&& worker.suicide !== true | ||
&& worker.state !== "none") { | ||
worker.pid = worker.process.pid; | ||
workers.push(worker); | ||
} | ||
} | ||
workers.send=send; | ||
workers.send = send; | ||
@@ -27,3 +33,3 @@ return workers; | ||
// i hate O(N) lookups, but not hit hard enough to worry about optimizing at | ||
// i hate O(N) lookups, but not hit hard enough to worry about optimizing at | ||
// this point. freshness is more important | ||
@@ -44,12 +50,15 @@ function getByPID(pid) { | ||
function getByPIDFromCache(pid) { | ||
return cservice.locals.workers[pid]; | ||
} | ||
function monitor() { | ||
process.on("message", function(msg) { | ||
if (!msg || typeof msg.cservice !== "string") { | ||
if (!cservice.msgBus.isValidMessage(msg)) { | ||
return; // end | ||
} | ||
switch (msg.cservice) { | ||
switch (msg.cservice.cmd) { | ||
case "processDetails": | ||
process.send({ | ||
cservice: "processDetails", | ||
process.send(cservice.msgBus.createMessage("processDetails", { | ||
processDetails: { | ||
@@ -61,3 +70,3 @@ memory: process.memoryUsage(), | ||
} | ||
}); | ||
})); | ||
break; | ||
@@ -68,2 +77,28 @@ } | ||
function demote() { | ||
// only demote if: | ||
// 1. process.getgid is defined (not Windows) | ||
// 2. Running as root | ||
// 3. workerGid is string and not a proxy worker | ||
var gid = cservice.options.workerGid || 'nobody'; | ||
var uid = cservice.options.workerUid || 'nobody'; | ||
if (process.getgid && process.getgid() === 0) { | ||
if ( // but do not auto-demote proxy | ||
// workers as they require priveledged port access | ||
cluster.worker.env.type !== "proxy" && | ||
typeof cservice.options.workerGid === 'string' | ||
) { | ||
process.setgid(gid); | ||
process.setuid(uid); | ||
} else { | ||
cservice.log( | ||
"Worker running as root. Not advised for Production." + | ||
" Consider workerGid & workerUid options.".warn | ||
); | ||
} | ||
} | ||
} | ||
/** | ||
@@ -77,3 +112,3 @@ * This is shorthand for: | ||
this.forEach(function(worker){ | ||
//worker.send.apply(worker, [].slice.apply(arguments)); | ||
worker.send.apply(worker, [].slice.apply(arguments)); | ||
}); | ||
@@ -84,3 +119,3 @@ } | ||
// inform the worker to exit gracefully | ||
worker.send({cservice: {cmd: "onWorkerStop"}}); | ||
worker.send(cservice.msgBus.createMessage("onWorkerStop")); | ||
} |
{ | ||
"name": "cluster-service", | ||
"version": "1.0.5", | ||
"version": "2.0.0-alpha1", | ||
"author": { | ||
@@ -20,13 +20,14 @@ "name": "Aaron Silvas", | ||
"dependencies": { | ||
"async": "~0.8.0", | ||
"optimist": ">=0.6.0", | ||
"colors": ">=0.6.2", | ||
"extend": ">=1.1.x" | ||
"async": "^0.9.0", | ||
"colors": "^1.0.3", | ||
"http-proxy": "^1.10.1", | ||
"optimist": "^0.6.1" | ||
}, | ||
"devDependencies": { | ||
"mocha": "~1.12.0", | ||
"request": ">=2.21.0", | ||
"istanbul": "~0.1.43", | ||
"sinon": "1.7.3", | ||
"jshint": "2.3.x" | ||
"6to5": "^3.6.5", | ||
"istanbul": "^0.3.13", | ||
"jshint": "^2.7.0", | ||
"mocha": "^2.2.4", | ||
"request": "^2.55.0", | ||
"sinon": "^1.14.1" | ||
}, | ||
@@ -33,0 +34,0 @@ "repository": { |
111
README.md
@@ -28,4 +28,4 @@ # cluster-service | ||
http://x.co/bpnodevid | ||
## Getting Started | ||
@@ -81,3 +81,3 @@ | ||
help {command} | ||
We can also issue commands from a seperate process, or even a remote machine (assuming proper access): | ||
@@ -100,3 +100,3 @@ | ||
cservice "server.js" --accessKey "123" | ||
cservice "server.js" --accessKey "123" | ||
@@ -156,5 +156,7 @@ Or via JSON config: | ||
* `master` - An optional module to execute for the master process only, once ```start``` has been completed. | ||
* `proxy` - Optional path to a JSON config file to enable Proxy Support. | ||
* `workerGid` - Group ID to assign to child worker processes (recommended `nobody`). | ||
* `workerUid` - User ID to assign to child worker processes (recommended `nobody`). | ||
## Console & REST API | ||
@@ -184,5 +186,5 @@ | ||
cservice --run "help" --accessKey "lksjdf982734" | ||
## Cluster Commands | ||
@@ -193,8 +195,8 @@ | ||
* `start workerPath [cwd] { [timeout:60] }` - Gracefully start service, one worker at a time. | ||
* `restart all|pid { [timeout:60] }` - Gracefully restart service, waiting up to timeout before terminating workers. | ||
* `shutdown all|pid { [timeout:60] }` - Gracefully shutdown service, waiting up to timeout before terminating workers. | ||
* `start workerPath [cwd] { [timeout:60000] }` - Gracefully start service, one worker at a time. | ||
* `restart all|pid { [timeout:60000] }` - Gracefully restart service, waiting up to timeout before terminating workers. | ||
* `shutdown all|pid { [timeout:60000] }` - Gracefully shutdown service, waiting up to timeout before terminating workers. | ||
* `exit now` - Forcefully exits the service. | ||
* `help [cmd]` - Get help. | ||
* `upgrade all|pid workerPath { [cwd] [timeout:60] }` - Gracefully upgrade service, one worker at a time. (continuous deployment support). | ||
* `upgrade all|pid workerPath { [cwd] [timeout:60000] }` - Gracefully upgrade service, one worker at a time. (continuous deployment support). | ||
* `workers` - Returns list of active worker processes. | ||
@@ -219,8 +221,8 @@ * `health` - Returns health of service. Can be overidden by service to expose app-specific data. | ||
}; | ||
cservice.on("test", function(evt, cb, testScript, timeout) { // we're overriding the "test" command | ||
// arguments | ||
// do something, no callback required (events may optionally be triggered) | ||
}; | ||
}; | ||
// can also issue commands programatically | ||
@@ -263,5 +265,5 @@ cservice.trigger("custom", function(err) { /* my callback */ }, "arg1value", "arg2value"); | ||
}); | ||
## Access Control | ||
@@ -286,4 +288,71 @@ | ||
## Proxy Support | ||
Proxy mode specifically caters to Web Servers that you want to enable automatic | ||
versioning of your service. Any version requested (via `versionHeader`) that is | ||
not yet loaded will automatically have a worker process spun up with the new | ||
version, and after ready, the proxy will route to that worker. | ||
Every version of your app *must* adhere to the `PROXY_PORT` environment | ||
variable like so: | ||
require("http").createServer(function(req, res) { | ||
res.writeHead(200); | ||
res.end("Hello world!"); | ||
}).listen(process.env.PROXY_PORT || 3000 /* port to use when not running in proxy mode */); | ||
### Proxy Options | ||
* `versionPath` (default: same directory as proxy JSON config) - Can override | ||
to point to a new version folder. | ||
* `defaultVersion` - The version (folder name) that is currently active/live. | ||
If you do not initially set this option, making a request to the Proxy without | ||
a `versionHeader` will result in a 404 (Not Found) since there is no active/live | ||
version. | ||
Upgrades will automatically update this option to the latest upgraded version. | ||
* `versionHeader` (default: `x-version`) - HTTP Header to use when determining | ||
non-default version to route to. | ||
* `workerFilename` (default: `worker.js`) - Filename of worker file. | ||
* `bindings` (default: `[{ port: 80, workerCount: 2 }]`) - An array of `Proxy Bindings`. | ||
* `versionPorts` (default: `11000-12000`) - Reserved port range that can be used to | ||
assign ports to different App versions via `PROXY_PORT`. | ||
* `nonDefaultWorkerCount` (default: 1) - If a version is requested that is not | ||
a default version, this will be the number of worker processes dedicated to | ||
that version. | ||
* `nonDefaultWorkerIdleTime` (default: 3600) - The number of seconds of inactivity | ||
before a non-default version will have its workers shut down. | ||
### Proxy Bindings | ||
Binding options: | ||
* `port` - Proxy port to bind to. | ||
* `workerCount` (default: 2) - Number of worker processes to use for this | ||
binding. Typically more than 2 is unnecessary for a proxy, and less than 2 | ||
is a potential failure point if a proxy worker ever goes down. | ||
* `tlsOptions` - TLS Options if binding for HTTPS. | ||
* `key` - Filename that contains the Certificate Key. | ||
* `cert` - Filename that contains the Certificate. | ||
* `pem` - Filename that contains the Certificate PEM if applicable. | ||
A full list of TLS Options: https://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener | ||
### Proxy Commands | ||
Work like any other `Cluster Commands`. | ||
* `proxy start configPath` - Start the proxy using the provided JSON config file. | ||
* `proxy stop` - Shutdown the proxy service. | ||
* `proxy version workerVersion workerCount` - Set a given App version to the | ||
desired number of worker processes. If the version is not already running, | ||
it will be started. If 2 workers for the version are already running, and you | ||
request 4, 2 more will be started. If 4 workers for the version are already | ||
running, and you request 2, 2 will be stopped. | ||
* `proxy promote workerVersion workerCount` - Workers identical to the | ||
`proxy version` command, except this will flag the version as active/live, | ||
resulting in the Proxy Config file being updated with the new `defaultVersion`. | ||
* `proxy info` - Fetch information about the proxy service. | ||
## Tests & Code Coverage | ||
@@ -297,3 +366,3 @@ | ||
Now test: | ||
Now test: | ||
@@ -306,3 +375,3 @@ npm test | ||
## Change Log | ||
@@ -312,6 +381,6 @@ | ||
## License | ||
[MIT](https://github.com/godaddy/node-cluster-service/blob/master/LICENSE.txt) |
var cservice = require("../cluster-service"); | ||
var assert = require("assert"); | ||
var httpclient = require("../lib/http-client"); | ||
var extend = require("extend"); | ||
var util = require("util"); | ||
var request = require("request"); | ||
@@ -34,3 +34,3 @@ | ||
httpclient.init( | ||
extend(cservice.options, {accessKey: "123", silentMode: true}) | ||
util._extend(cservice.options, {accessKey: "123", silentMode: true}) | ||
); | ||
@@ -37,0 +37,0 @@ it('Health check', function(done) { |
@@ -83,7 +83,10 @@ var cservice = require("../cluster-service"); | ||
it('Stop workers', function(done) { | ||
cservice.stop(30000, function() { | ||
cservice.stop(30000, function(err, msg) { | ||
assert.ok(!err, 'Error: ' + err); | ||
assert.equal( | ||
cservice.workers.length, | ||
0, | ||
"0 workers expected, but " + cservice.workers.length + " found" | ||
"0 workers expected, but " | ||
+ cservice.workers.length | ||
+ " found. Message: " + msg | ||
); | ||
@@ -90,0 +93,0 @@ done(); |
@@ -128,8 +128,11 @@ var cservice = require("../cluster-service"); | ||
it('Stop workers', function(done) { | ||
cservice.stop(30000, function() { | ||
it('Stop workers after upgrade', function(done) { | ||
cservice.stop(30000, function(err, msg) { | ||
assert.ok(!err, 'Received error ' + err); | ||
assert.equal( | ||
cservice.workers.length, | ||
0, | ||
"0 workers expected, but " + cservice.workers.length + " found" | ||
"0 workers expected, but " | ||
+ cservice.workers.length | ||
+ " found. Message: " + msg | ||
); | ||
@@ -180,7 +183,10 @@ done(); | ||
it('Stop workers', function(done) { | ||
cservice.stop(30000, function() { | ||
cservice.stop(30000, function(err, msg) { | ||
assert.ok(!err, 'Received error ' + err); | ||
assert.equal( | ||
cservice.workers.length, | ||
0, | ||
"0 workers expected, but " + cservice.workers.length + " found" | ||
"0 workers expected, but " | ||
+ cservice.workers.length | ||
+ " found. Message: " + msg | ||
); | ||
@@ -191,2 +197,2 @@ done(); | ||
}); | ||
} | ||
} |
var cservice = require("../../cluster-service"); | ||
cservice.workerReady(false); | ||
setTimeout(function() { | ||
cservice.workerReady(); | ||
}, 10000); |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
157094
88
4038
375
6
1
15
14
+ Addedhttp-proxy@^1.10.1
+ Addedasync@0.9.2(transitive)
+ Addedeventemitter3@4.0.7(transitive)
+ Addedfollow-redirects@1.15.9(transitive)
+ Addedhttp-proxy@1.18.1(transitive)
+ Addedrequires-port@1.0.0(transitive)
- Removedextend@>=1.1.x
- Removedasync@0.8.0(transitive)
- Removedextend@3.0.2(transitive)
Updatedasync@^0.9.0
Updatedcolors@^1.0.3
Updatedoptimist@^0.6.1