UNPKG

combohandler

Version:

Simple Yahoo!-style combo handler.

313 lines (260 loc) 9.8 kB
/** ComboHandler.Cluster.Master **/ var cluster = require('cluster'); var util = require('util'); var ComboBase = require('./base'); var checkStatus = require('./status'); var pidfiles = require('./pidfiles'); exports = module.exports = ComboCluster; /** A factory for running combohandler in multiple processes. @class ComboCluster @extends ComboBase @constructor @param {Object} [options] @param {Function} [cb] **/ function ComboCluster(options, cb) { // factory constructor if (!(this instanceof ComboCluster)) { return new ComboCluster(options, cb); } if ("function" === typeof options) { cb = options; options = {}; } ComboBase.call(this, options); // this doesn't work in OS X (node-v0.8.x), but whatever this.process.title = 'combohandler master'; this.startupTimeout = []; this.closingTimeout = []; this.flameouts = 0; // TODO: configurable // for test stubbing this.cluster = cluster; this.pidutil = pidfiles; this._maybeCallback(cb); } util.inherits(ComboCluster, ComboBase); ComboCluster.defaults = require('../defaults').master; ComboCluster.prototype._start = function (cb) { this.setupMaster({ exec: __dirname + '/worker.js' }); this._setupMasterPidFile(); this._attachEvents(); this._maybeCallback(cb); }; ComboCluster.prototype._destroy = function (cb) { this.cluster.disconnect(function () { this._detachEvents(); this._maybeCallback(cb); }.bind(this)); }; ComboCluster.prototype.setupMaster = function (options) { this.cluster.setupMaster(options); }; /** Attach listeners to cluster lifecycle events. @method _attachEvents @private **/ ComboCluster.prototype._attachEvents = function () { this._bindProcess(); this._bindCluster(); }; /** Detach listeners from cluster lifecycle events. @method _detachEvents @private **/ ComboCluster.prototype._detachEvents = function () { this.emit('cleanup'); }; ComboCluster.prototype._setupMasterPidFile = function () { this.pidutil.writePidFileSync(this.options.pids, 'master', this.process.pid); }; ComboCluster.prototype._bindProcess = function () { var self = this; // bind listeners to access this.options.pids var boundGracefulShutdown = this.gracefulShutdown.bind(this); var boundRestartWorkers = this.restartWorkers.bind(this); // Signal Event Handlers // http://nodejs.org/api/process.html#process_signal_events // http://en.wikipedia.org/wiki/Unix_signal#List_of_signals this.process.on('SIGINT', boundGracefulShutdown) .on('SIGTERM', boundGracefulShutdown) .on('SIGUSR2', boundRestartWorkers); this.once('cleanup', function () { self.process.removeListener('SIGINT', boundGracefulShutdown) .removeListener('SIGTERM', boundGracefulShutdown) .removeListener('SIGUSR2', boundRestartWorkers); }); }; ComboCluster.prototype._bindCluster = function () { var self = this; var boundForked = this._workerForked.bind(this); var boundOnline = this._workerOnline.bind(this); var boundListening = this._workerListening.bind(this); var boundDisconnected = this._workerDisconnected.bind(this); var boundExited = this._workerExited.bind(this); this.cluster.on('fork', boundForked) .on('online', boundOnline) .on('listening', boundListening) .on('disconnect', boundDisconnected) .on('exit', boundExited); this.once('cleanup', function () { self.cluster.removeListener('fork', boundForked) .removeListener('online', boundOnline) .removeListener('listening', boundListening) .removeListener('disconnect', boundDisconnected) .removeListener('exit', boundExited); }); }; ComboCluster.prototype._workerForked = function (worker) { this.startupTimeout[worker.id] = setTimeout(function () { console.error('Something is wrong with worker %d', worker.id); }, this.options.timeout); }; ComboCluster.prototype._workerOnline = function (worker) { console.error('Worker %d online', worker.id); clearTimeout(this.startupTimeout[worker.id]); worker.send({ cmd: 'listen', data: this.options }); }; ComboCluster.prototype._workerListening = function (worker) { console.error('Worker %d listening with pid %d', worker.id, worker.process.pid); clearTimeout(this.startupTimeout[worker.id]); // this doesn't work in OS X, but whatever worker.process.title = 'combohandler worker'; this.pidutil.writePidFileSync(this.options.pids, 'worker' + worker.id, worker.process.pid); }; ComboCluster.prototype._workerDisconnected = function (worker) { console.error('Worker %d disconnecting...', worker.id); this.closingTimeout[worker.id] = setTimeout(function () { worker.destroy(); console.error('Forcibly destroyed worker %d', worker.id); }, this.options.timeout); }; ComboCluster.prototype._workerExited = function (worker, code, signal) { clearTimeout(this.startupTimeout[worker.id]); clearTimeout(this.closingTimeout[worker.id]); if (worker.suicide) { console.error('Worker %d exited cleanly.', worker.id); this.pidutil.removePidFileSync(this.options.pids, 'worker' + worker.id); } else { if (signal) { console.error('Worker %d received signal %s', worker.id, signal); if (signal === 'SIGUSR2') { console.error('Worker %d restarting, removing old pidfile', worker.id); this.pidutil.removePidFileSync(this.options.pids, 'worker' + worker.id); } } if (code) { console.error('Worker %d exited with code %d', worker.id, code); if (++this.flameouts > 20) { console.error("Too many errors during startup, bailing!"); this.pidutil.removePidFileSync(this.options.pids, 'master'); return this.process.exit(1); } } console.error('Worker %d died, respawning!', worker.id); this.cluster.fork(); } }; // SIGINT (Ctrl+C) // SIGTERM (default signal from `kill`) ComboCluster.prototype.gracefulShutdown = function () { console.log('combohandler master %d shutting down...', this.process.pid); var self = this; this.cluster.disconnect(function () { self.process.once('exit', function () { self.pidutil.removePidFileSync(self.options.pids, 'master'); console.log('combohandler master %d finished shutting down!', self.process.pid); }); }); }; // SIGUSR2 ComboCluster.prototype.restartWorkers = function () { console.log('combohandler master %d restarting workers...', this.process.pid); /*jshint forin:false */ for (var id in this.cluster.workers) { this.process.kill(this.cluster.workers[id].process.pid, 'SIGUSR2'); } }; ComboCluster.prototype._signalMaster = function (signal) { var self = this; this.pidutil.getMasterPid(this.options.pids, function (err, masterPid, dir) { if (err) { console.error("Error sending signal %s to combohandler master process", signal); if ('ENOENT' === err.code) { console.error('combohandler master not running!'); self.process.exit(1); } else { throw err; } } else { try { // again, because SIGKILL is so incredibly rude, // he doesn't allow us to do anything afterward if ('SIGKILL' === signal) { self.pidutil.removePidFileSync(dir, 'master'); } // send signal to master process, not necessarily "killing" it self.process.kill(masterPid, signal); } catch (ex) { if ('ESRCH' === ex.code) { console.error('combohandler master not running!'); } else { throw ex; } } } }); }; ComboCluster.prototype.restart = function () { this._signalMaster('SIGUSR2'); }; ComboCluster.prototype.status = function () { var self = this; this.pidutil.getMasterPid(this.options.pids, function (err, masterPid, dir) { if (err) { if ('ENOENT' === err.code) { console.error('combohandler master not running!'); return self.process.exit(1); } else { throw err; } } self.pidutil.getWorkerPidsSync(dir).forEach(checkStatus.bind(null, 'worker')); // check master last so we can exit non-zero when dead checkStatus('master', masterPid); }); }; ComboCluster.prototype.shutdown = function () { this._signalMaster('SIGTERM'); }; ComboCluster.prototype.stop = function () { // must clean up worker pid files before sending SIGKILL, // because SIGKILL listeners basically can't do anything this.pidutil.removeWorkerPidFiles(this.options.pids, this._stop.bind(this)); }; ComboCluster.prototype._stop = function () { console.error('combohandler master %d stopping abruptly...', this.process.pid); this._signalMaster('SIGKILL'); }; // _collaborate ComboCluster.prototype._listen = function () { // fork console.log('Forking workers from combohandler master %d', this.process.pid); // console.log(this.options); var workers = this.options.workers; while (workers--) { this.cluster.fork(); } };