UNPKG

clusterjs

Version:

Clusterify your NodeJS applications and deploy without downtimes. Just like that.

202 lines (173 loc) 5.41 kB
var cluster = require('cluster'); var path = require('path'); var fs = require('fs'); var _ = require('lodash'); var deaths = []; /** * Stops a worker, waits for it to disconnect * and gives control back. * * @param workerKey * @param callback */ function stopWorker(workerKey, callback){ cluster.workers[workerKey].disconnect(); cluster.workers[workerKey].on("disconnect", function () { console.log("Shutdown complete for worker " + workerKey); callback(); }); } /** * Restarts a list of clustered server. * waits for each worker server to come up * so we have 0 downtime. * * @param {Array} workers */ function reloadWorkers(workers) { console.log('*** reloading workers!'); var workerKey = workers.shift(); console.log('restarting worker: ' + workerKey); stopWorker(workerKey, function () { var newWorker = cluster.fork(); newWorker.on("listening", function () { console.log("Replacement worker online."); if (workers.length > 0) { reloadWorkers(workers); } }); }); } /** * stops every worker one at a time waiting before killing the next * * @param workers */ function shutdownWorkers(workers) { var workerKey = workers.shift(); console.log('shutting down worker: ' + workerKey); stopWorker(workerKey, function () { if (workers.length > 0) { shutdownWorkers(workers); } else { process.exit(); } }); } /** * Figures out the correct path for the app. * * @param appPath * @returns {*} */ function findAppScript(appPath) { var prefix = (appPath.indexOf('/') !== 0) ? process.cwd() : ''; appPath = path.join(prefix, appPath); if (fs.existsSync(appPath)) { return appPath; } else { throw new Error('Cannot find required app'); } } /** * Calculates the delay we should wait for * when booting a replacement worker. * Sometimes your scripts cannot really start * (DB is down, etc) and clusterjs ends * up consuming a lot of CPU trying to * restart them forever. * * If your workers die too fast, we will * wait 250ms for each of them that died * in the past 10s. * To optimize calculations, we delete deaths * that happened more than 10s ago. * * If only 1 worker has died in the past 10s, * we skip the delay as it might be a * genuine / regular death. */ function calculateDelay() { var interval = 250; var now = (new Date()).getTime(); var delay = _.sum(_.map(deaths, function(death, index){ if (now - death < 10000) { return interval; } deaths.splice(index, 1); return 0; })); if (delay === interval) { delay = 0; } console.log('Going to wait ' + delay / 1000 + ' seconds before booting a new worker as we died ' + deaths.length + ' times in the past seconds'); deaths.push(now) return delay; } /** * Takes care of launching our cluster * master process and it's children * * @param {String} appPath * @param {Number} noOfWorkers * @param {String} reloadSignal */ function launch (appPath, noOfWorkers, reloadSignal) { if (appPath === null) { throw new Error('Please provide a path to your app. You can either pass it as a parameter or as process.env.APP'); } appPath = findAppScript(appPath || process.env.APP); noOfWorkers = noOfWorkers || require('os').cpus().length; reloadSignal = reloadSignal || "SIGUSR2"; console.log('appPath', appPath); if (cluster.isMaster) { // Create a worker for each CPU for (var i = 0; i < noOfWorkers; i += 1) { cluster.fork(); } // Listen for dying workers cluster.on('exit', function (worker) { // A suicide means we shutdown the worker on purpose // like in a deployment if (worker.suicide !== true) { setTimeout(function(){ console.log('Worker ' + worker.id + ' died :( ...booting a replacement worker'); cluster.fork(); }, calculateDelay()); } }); process.on(reloadSignal, function () { console.log(reloadSignal + ' received, reloading the app'); // delete the cached module, so we can reload the app delete require.cache[require.resolve(appPath)]; console.log('The app has been reloaded'); // only reload one worker at a time // otherwise, we'll have a time when no request handlers are running var workers = Object.keys(cluster.workers); reloadWorkers(workers); }); } else { var app = require(appPath); console.log('Worker ' + cluster.worker.id + ' running!'); } } /** * lists the workers and inits the graceful shutdown procedure */ function gracefulShutdown(){ console.log('*** shutting down gracefully'); var workers = Object.keys(cluster.workers); shutdownWorkers(workers); } /** * This is how the world sees us * * @param {String} appPath * @param {Number} noOfWorkers * @param {String} reloadSignal */ module.exports = function(appPath, noOfWorkers, reloadSignal) { launch(appPath, noOfWorkers, reloadSignal); process.on('SIGTERM', gracefulShutdown); process.on('SIGQUIT', gracefulShutdown); };