cluster-service
Version:
Turns your single process code into a fault-resilient multi-process service with built-in REST & CLI support
134 lines (112 loc) • 3.78 kB
JavaScript
var async = require("async"),
util = require("util"),
cservice = require("../../cluster-service");
module.exports = function(evt, cb, cmd, workerPath, options) {
var pid = parseInt(cmd);
var originalAutoRestart;
var tasks;
var workerOptions;
options = options || {};
options.timeout = parseInt(options.timeout) || 60000;
options.worker = workerPath;
if (typeof workerPath !== "string" || (cmd !== "all" && !pid)) {
cb("Invalid request. Try help upgrade");
return;
}
evt.locals.reason = "upgrade";
originalAutoRestart = evt.locals.restartOnFailure;
evt.locals.restartOnFailure = false;
tasks = [];
evt.service.workers.forEach(function(worker){
if (pid && worker.process.pid !== pid) {
return; // cannot kill external processes
}
// use original worker options as default, by overwrite using new options
workerOptions = util._extend(util._extend({}, worker.cservice), options);
tasks.push(getTask(evt, worker, workerOptions));
});
if (tasks.length === 0) {
cb("No workers to upgrade");
} else {
cservice.log("Upgrading workers... timeout: " + (options.timeout || 0));
var limit = 1 +
Math.floor(
tasks.length * cservice.options.restartConcurrencyRatio
);
async.parallelLimit(tasks, limit, function(err) {
evt.locals.restartOnFailure = originalAutoRestart;
if (err) {
cb(err);
} else {
cb(null, tasks.length + " workers upgraded successfully");
}
});
}
};
module.exports.more = function(cb) {
cb(null, {
info: "Gracefully upgrade service, one worker at a time.",
command: "upgrade all|pid workerPath { \"option1\": \"value\" }",
"all|pid": [
"Required. 'all' to force shutdown of all workers, otherwise the pid of",
"the specific worker to upgrade"
].join(' '),
"workerPath":[
"Path of worker file (i.e. /workers/worker) to start, absolute path, or",
"relative to cwd."
].join(' '),
"options": "An object of options.",
"* cwd":[
"Path to set as the current working directory. If not provided, existing",
"cwd will be used."
].join(' '),
"* timeout": [
"Timeout, in milliseconds, before terminating replaced workers. 0 for",
"infinite wait."
].join(' ')
});
};
function getTask(evt, worker, options) {
return function(cb) {
var pendingWorker;
// kill new worker if takes too long
var newWorkerTimeout = null;
var isNewWorkerTerminated = false;
if (options.timeout > 0) { // start timeout if specified
newWorkerTimeout = setTimeout(function() {
if (!pendingWorker) return;
isNewWorkerTerminated = true;
pendingWorker.on('exit', function () {
cb("timed out");
});
pendingWorker.kill("SIGKILL"); // go get'em, killer
}, options.timeout);
}
// lets start new worker
pendingWorker = evt.service.newWorker(options, function (err) {
pendingWorker = null;
if (newWorkerTimeout) { // timeout no longer needed
clearTimeout(newWorkerTimeout);
}
if (err) {
cb(err);
return;
}
// ok, lets stop old worker
var oldWorkerTimeout = null;
if (options.timeout > 0) { // start timeout if specified
oldWorkerTimeout = setTimeout(function() {
worker.kill("SIGKILL"); // go get'em, killer
}, options.timeout);
}
worker.on("exit", function() {
if (oldWorkerTimeout) {
clearTimeout(oldWorkerTimeout);
}
// exit complete, fire callback
setImmediate(cb); // slight delay in case other events are piled up
});
require("../workers").exitGracefully(worker);
});
};
}