node-clusterprocess
Version:
Wrapper around Node.js' cluster module for zero-downtime deployment of Node.js Apps. By entrecode.
178 lines (147 loc) • 5.24 kB
JavaScript
/**
* clusterprocess module
*
* @author deyhle@entrecode.de
*
* Used for startup and managing worker processes.
* Loosely based on https://gist.github.com/dickeyxxx/0f535be1ada0ea964cae – more info:
* http://blog.carbonfive.com/2014/06/02/node-js-in-production/
*
* This script will boot a given app.js with the number of available CPUs or NODE_CLUSTER_MAX_WORKER.
* The master will respond to SIGHUP, which will trigger restarting all the workers and reloading
* the app.
*/
const cluster = require('cluster');
const os = require('os');
const path = require('path');
let numWorker = os.cpus().length;
let logger = console;
if (process.env.NODE_CLUSTER_MAX_WORKER) {
numWorker = Number.parseInt(process.env.NODE_CLUSTER_MAX_WORKER, 10);
}
const ClusterProcess = {
run(executable, name) {
if (!executable) {
logger.error('No executable specified (first parameter)');
process.exit(1);
}
let processname = name;
if (!processname) {
const packageJsonOfParentProject = require(path.resolve(__dirname, '../../package'));
if (packageJsonOfParentProject.name) {
processname = packageJsonOfParentProject.name;
} else {
logger.error('No project name found. Specify as second parameter or as "name" in your project\'s package.json');
// eslint-disable-next-line no-process-exit
process.exit(1);
}
}
process.title = `${processname}_cp`;
// Defines what each worker needs to run
cluster.setupMaster({ exec: path.resolve(__dirname, '../../', executable) });
// Gets the count of active workers
function numWorkers() {
return Object.keys(cluster.workers).length;
}
let stopping = false;
// Forks off the workers unless the server is stopping
function forkNewWorkers(worker) {
if (!stopping) {
const timeout = worker && !worker.exitedAfterDisconnect ? 2000 : 0;
setTimeout(() => {
for (let i = numWorkers(); i < numWorker; i += 1) {
cluster.fork();
}
}, timeout);
}
}
// A list of workers queued for a restart
let workersToStop = [];
// Stops a single worker
// Gives 10 seconds after SIGTERM before SIGTERM
function stopWorker(worker) {
logger.info('stopping worker', worker.process.pid);
worker.kill('SIGTERM');
const killTimer = setTimeout(() => {
worker.kill('SIGKILL');
}, 10000);
// Ensure we don't stay up just for this setTimeout
killTimer.unref();
}
// Tell the next worker queued to restart to disconnect
// This will allow the process to finish it's work
// for 20 seconds before sending SIGTERM
function stopNextWorker() {
const i = workersToStop.pop();
if (cluster.workers[i]) {
stopWorker(cluster.workers[i]);
}
}
// Stops all the works at once
function stopAllWorkers() {
stopping = true;
logger.info('stopping all workers');
Object.keys(cluster.workers).forEach((id) => {
stopWorker(cluster.workers[id]);
});
}
// Worker is now listening on a port
// Once it is ready, we can signal the next worker to restart
cluster.on('listening', stopNextWorker);
// A worker has disconnected either because the process was killed
// or we are processing the workersToStop array restarting each process
// In either case, we will fork any workers needed
// cluster.on('disconnect', forkNewWorkers);
cluster.on('exit', forkNewWorkers); // only after disconnect AND exit have fired, the worker
// count is correct. So better safe than sorry
// HUP signal sent to the master process to start restarting all the workers sequentially
process.on('SIGHUP', () => {
logger.info('restarting all workers');
workersToStop = Object.keys(cluster.workers);
stopNextWorker();
});
// Kill all the workers at once
process.on('SIGTERM', stopAllWorkers);
// Fork off the initial workers
forkNewWorkers();
logger.info(
`${process.title} booted. pid is ${process.pid} scheduling is ${
cluster.schedulingPolicy === cluster.SCHED_RR ? 'round-robin' : 'off (system)'
}`,
);
return this;
},
setLogger(loggingInstance) {
if (
typeof loggingInstance.log === 'function' &&
typeof loggingInstance.info === 'function' &&
typeof loggingInstance.warn === 'function' &&
typeof loggingInstance.error === 'function'
) {
logger = loggingInstance;
return this;
}
logger.error('Could not attach new logger because of missing methods.');
return this;
},
handleSignals(cleanFunc) {
const func = cleanFunc || (() => undefined);
if (typeof func !== 'function') {
throw new Error('cleanFunc must be a function');
}
process.on('SIGHUP', () => {
logger.info(`${process.pid} got SIGHUP`);
func();
});
process.on('SIGINT', () => {
logger.info(`${process.pid} got SIGINT`);
func();
});
process.on('SIGTERM', () => {
logger.info(`${process.pid} got SIGTERM`);
func();
});
return this;
},
};
module.exports = ClusterProcess;