123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917 |
- /*!
- * Cluster - Master
- * Copyright(c) 2011 LearnBoost <dev@learnboost.com>
- * MIT Licensed
- */
- /**
- * Module dependencies.
- */
- var Worker = require('./worker')
- , EventEmitter = require('events').EventEmitter
- , dirname = require('path').dirname
- , spawn = require('child_process').spawn
- , utils = require('./utils')
- , fsBinding = process.binding('fs')
- , netBinding = process.binding('net')
- , bind = netBinding.bind
- , listen = netBinding.listen
- , socket = netBinding.socket
- , socketpair = netBinding.socketpair
- , close = netBinding.close
- , unlink = fsBinding.unlink
- , dgram = require('dgram')
- , tty = require('tty')
- , net = require('net')
- , fs = require('fs')
- , os = require('os');
- /**
- * Node binary.
- */
- var node = process.execPath;
- /**
- * Start a new `Master` with the given `server` or filename to
- * a node module exporting a server.
- *
- * Options:
- *
- * - `workers` Number of workers to spawn, defaults to the number of CPUs
- * - 'working directory` Working directory defaulting to the script's dir
- * - 'backlog` Connection backlog, defaulting to 128
- * - 'socket port` Master socket port defaulting to `8989`
- * - 'timeout` Worker shutdown timeout in milliseconds, defaulting to 60,000
- * - 'user` User id / name
- * - 'group` Group id / name
- * - `title` Master process title, defaults to "cluster master"
- * - `worker title` Worker process title, defaults to "cluster worker {n}"
- *
- * Events:
- *
- * - `start`. When the IPC server is prepped
- * - `worker`. When a worker is spawned, passing the `worker`
- * - `listening`. When the server is listening for connections
- * - `closing`. When master is shutting down
- * - `close`. When master has completed shutting down
- * - `worker killed`. When a worker has died
- * - `worker exception`. Worker uncaughtException. Receives the worker / exception
- * - `worker removed`. Worker removed via `spawn(-n)`
- * - `kill`. When a `signal` is being sent to all workers
- * - `restarting`. Restart requested by REPL or signal. Receives an object
- * which can be patched in order to preserve plugin state.
- * - `restart`. Restart complete, new master established, previous died.
- * Receives an object with state preserved by the `restarting` event.
- *
- * Signals:
- *
- * - `SIGINT` hard shutdown
- * - `SIGTERM` hard shutdown
- * - `SIGQUIT` graceful shutdown
- * - `SIGUSR2` graceful restart
- *
- * @param {net.Server|String} server
- * @return {Master}
- * @api public
- */
- var Master = module.exports = function Master(server) {
- var self = this;
- this.server = server;
- this.plugins = [];
- this.children = [];
- this.state = 'active';
- this.startup = new Date;
- this._killed = 0;
- // grab server root
- this.cmd = process.argv.slice(1);
- this.dir = dirname(this.cmd[0]);
- // environment
- this.env = process.env.NODE_ENV || 'development';
- // defaults
- this.options = {
- 'backlog': 128
- , 'working directory': this.dir
- , 'socket port': 8989
- , 'socket addr': '127.0.0.1'
- , 'timeout': 60000
- , 'restart threshold': 'development' == this.env ? 5000 : 60000
- , 'restart timeout': 'development' == this.env ? 5000 : 60000
- , 'title': 'cluster'
- , 'worker title': 'cluster worker'
- };
- // parent master pid
- this.ppid = process.env.CLUSTER_PARENT_PID
- ? parseInt(process.env.CLUSTER_PARENT_PID, 10)
- : null;
- // process is a worker
- this.isWorker = !! process.env.CLUSTER_MASTER_PID;
- // process is a child (worker or master replacement)
- this.isChild = this.isWorker || !! process.env.CLUSTER_REPLACEMENT_MASTER;
- // process is master
- this.isMaster = ! this.isWorker;
- // process id
- this.pid = process.pid;
- if (this.isMaster) process.env.CLUSTER_MASTER_PID = this.pid;
- // custom worker fds, defaults to std{out,err}
- this.customFds = [1, 2];
- // resolve server filename
- if (this.isWorker && 'string' == typeof this.server) {
- this.server = require(this.resolve(this.server));
- }
- // IPC is prepped
- this.on('start', function(){
- process.chdir(self.options['working directory']);
- });
- // spawn our workers
- this.on('listening', function(){
- self.spawn(self.options.workers);
- self.listening = true;
- });
- // kill children on master exception
- if (this.isMaster) {
- process.on('uncaughtException', function(err){
- self.kill('SIGKILL');
- console.error(err.stack || String(err));
- process.exit(1);
- });
- }
- };
- /**
- * Interit from `EventEmitter.prototype`.
- */
- Master.prototype.__proto__ = EventEmitter.prototype;
- /**
- * Worker is a receiver.
- */
- require('./mixins/receiver')(Master.prototype);
- /**
- * Resolve `path` relative to the server file being executed.
- *
- * @param {String} path
- * @return {String}
- * @api public
- */
- Master.prototype.resolve = function(path){
- return '/' == path[0]
- ? path
- : this.dir + '/' + path;
- };
- /**
- * Return `true` when the environment set by `Master#in()`
- * matches __NODE_ENV__.
- *
- * @return {Boolean}
- * @api private
- */
- Master.prototype.__defineGetter__('environmentMatches', function(){
- if (this._env)
- return this.env == this._env || 'all' == this._env;
- return true;
- });
- /**
- * Invoke masters's `method` with worker `id`. (called from Worker)
- *
- * @param {Number} id
- * @param {String} method
- * @param {...} args
- * @api private
- */
- Master.prototype.call = function(id, method){
- this.sock = this.sock || dgram.createSocket('udp4');
- var msg = new Buffer(utils.frame({
- args: utils.toArray(arguments, 2)
- , method: method
- , id: id
- }));
- this.sock.send(
- msg
- , 0
- , msg.length
- , this.options['socket port']
- , this.options['socket addr']);
- };
- /**
- * Perform setup tasks then invoke `fn()` when present.
- *
- * @param {Function} fn
- * @return {Master} for chaining
- * @api public
- */
- Master.prototype.start = function(fn){
- var self = this;
- // deferred title
- process.title = this.options.title;
- // prevent listen
- if (this.preventDefault) return this;
- // env match
- if (this.environmentMatches) {
- // worker process
- if (this.isWorker) {
- this.worker = new Worker(this);
- this.worker.start();
- // master process
- } else if (fn) {
- fn();
- // standalone
- } else {
- this.on('start', function(){ self.emit('listening'); });
- if (this.isChild) this.acceptFd();
- this.setupIPC();
- }
- }
- return this;
- };
- /**
- * Defer `http.Server#listen()` call.
- *
- * @param {Number|String} port or unix domain socket path
- * @param {String|Function} host or callback
- * @param {Function} callback
- * @return {Master} for chaining
- * @api public
- */
- Master.prototype.listen = function(port, host, callback){
- var self = this;
- if (!this.environmentMatches) return this;
- if ('function' == typeof host) callback = host, host = null;
- this.port = port;
- this.host = host;
- this.callback = callback;
- return this.start(function(){
- self.on('start', function(){
- self.startListening(!self.isChild);
- });
- if (self.isChild) {
- self.acceptFd();
- } else {
- self.createSocket(function(err, fd){
- if (err) throw err;
- self.fd = fd;
- self.setupIPC();
- });
- }
- });
- };
- /**
- * Create / return IPC socket.
- *
- * @api private
- */
- Master.prototype.IPCSocket = function(){
- var self = this;
- if (this._sock) return this._sock;
- this._sock = dgram.createSocket('udp4');
- this._sock.on('message', function(msg, info){
- try {
- msg = JSON.parse(msg.toString('ascii'));
- self.invoke(msg.method, msg.args, self.children[msg.id]);
- } catch (err) {
- console.error(err.stack || String(err));
- }
- });
- return this._sock;
- };
- /**
- * Setup IPC.
- *
- * @api private
- */
- Master.prototype.setupIPC = function(){
- var self = this;
- // signal handlers
- this.registerSignalHandlers();
- // Default worker to the # of cpus
- this.defaultWorkers();
- // udp server for IPC
- this.IPCSocket().on('listening', function(){
- process.nextTick(function(){
- self.emit('start');
- });
- });
- // bind
- this.IPCSocket().bind(
- this.options['socket port']
- , this.options['socket addr']);
- };
- /**
- * Conditionally perform the following action, if
- * __NODE_ENV__ matches `env`.
- *
- * Examples:
- *
- * cluster(server)
- * .in('development').use(cluster.debug())
- * .in('development').listen(3000)
- * .in('production').listen(80);
- *
- * @param {String} env
- * @return {Master} self or stubs
- * @api public
- */
- Master.prototype.in = function(env){
- this._env = env;
- return this;
- };
- /**
- * Set option `key` to `val`.
- *
- * @param {String} key
- * @param {Mixed} val
- * @return {Master} for chaining
- * @api public
- */
- Master.prototype.set = function(key, val){
- if (this.environmentMatches) this.options[key] = val;
- return this;
- };
- /**
- * Invoke `fn(master)`.
- *
- * @param {Function} fn
- * @api public
- */
- Master.prototype.do = function(fn){
- if (this.environmentMatches) fn.call(this, this);
- return this;
- };
- /**
- * Check if `option` has been set.
- *
- * @param {String} option
- * @return {Boolean}
- * @api public
- */
- Master.prototype.has = function(option){
- return !! this.options[option];
- };
- /**
- * Use the given `plugin`.
- *
- * @param {Function} plugin
- * @return {Master} for chaining
- * @api public
- */
- Master.prototype.use = function(plugin){
- if (this.environmentMatches) {
- this.plugins.push(plugin);
- if (this.isWorker) {
- plugin.enableInWorker && plugin(this);
- } else {
- plugin(this);
- }
- }
- return this;
- };
- /**
- * Create listening socket and callback `fn(err, fd)`.
- *
- * @return {Function} fn
- * @api private
- */
- Master.prototype.createSocket = function(fn){
- var self = this
- , ipv;
- // explicit host
- if (this.host) {
- // ip
- if (ipv = net.isIP(this.host)) {
- fn(null, socket('tcp' + ipv));
- // lookup
- } else {
- require('dns').lookup(this.host, function(err, ip, ipv){
- if (err) return fn(err);
- self.host = ip;
- fn(null, socket('tcp' + ipv));
- });
- }
- // local socket
- } else if ('string' == typeof this.port) {
- fn(null, socket('unix'));
- // only port
- } else if ('number' == typeof this.port) {
- fn(null, socket('tcp4'));
- }
- };
- /**
- * Register signal handlers.
- *
- * @api private
- */
- Master.prototype.registerSignalHandlers = function(){
- var self = this;
- process.on('SIGINT', this.destroy.bind(this));
- process.on('SIGTERM', this.destroy.bind(this));
- process.on('SIGQUIT', this.close.bind(this));
- process.on('SIGUSR2', this.attemptRestart.bind(this));
- process.on('SIGCHLD', this.maintainWorkerCount.bind(this));
- };
- /**
- * Default workers to the number of cpus available.
- *
- * @api private
- */
- Master.prototype.defaultWorkers = function(){
- if (!this.has('workers')) {
- this.set('workers', os
- ? os.cpus().length
- : 1);
- }
- };
- /**
- * Restart workers only, sending `signal` defaulting
- * to __SIGQUIT__.
- *
- * @param {Type} name
- * @return {Type}
- * @api public
- */
- Master.prototype.restartWorkers = function(signal){
- this.kill(signal || 'SIGQUIT');
- };
- /**
- * Maintain worker count, re-spawning if necessary.
- *
- * @api private
- */
- Master.prototype.maintainWorkerCount = function(){
- this.children.forEach(function(worker){
- var pid = worker.proc.pid;
- if (!pid) this.workerKilled(worker);
- }, this);
- };
- /**
- * Remove `n` workers with `signal`
- * defaulting to __SIGQUIT__.
- *
- * @param {Number} n
- * @param {String} signal
- * @api public
- */
- Master.prototype.remove = function(n, signal){
- if (!arguments.length) n = 1;
- var len = this.children.length
- , worker;
- // cap at worker len
- if (n > len) n = len;
- // remove the workers
- while (n--) {
- worker = this.children.pop();
- worker.proc.kill(signal || 'SIGQUIT');
- this.emit('worker removed', worker);
- this.removeWorker(worker.id);
- }
- };
- /**
- * Remove worker `id`.
- *
- * @param {Number} id
- * @api public
- */
- Master.prototype.removeWorker = function(id){
- var worker = this.children[id];
- if (!worker) return;
- if (worker.fds) {
- close(worker.fds[0]);
- close(worker.fds[1]);
- }
- delete this.children[id];
- };
- /**
- * Spawn `n` workers.
- *
- * @param {Number} n
- * @api public
- */
- Master.prototype.spawn = function(n){
- if (!arguments.length) n = 1;
- while (n--) this.spawnWorker();
- };
- /**
- * Spawn a worker with optional `id`.
- *
- * @param {Number} id
- * @return {Worker}
- * @api private
- */
- Master.prototype.spawnWorker = function(id){
- var worker;
- // id given
- if ('number' == typeof id) {
- worker = new Worker(this).spawn(id)
- this.children[id] = worker;
- worker.id = id;
- // generate an id
- } else {
- worker = new Worker(this).spawn(this.children.length);
- this.children.push(worker);
- }
- var obj = {
- method: 'connect'
- , args: [worker.id, this.options]
- };
- worker.sock.write(utils.frame(obj), 'ascii', this.fd);
- // emit
- this.emit('worker', worker);
- return worker;
- };
- /**
- * Graceful shutdown, wait for all workers
- * to reply before exiting.
- *
- * @api public
- */
- Master.prototype.close = function(){
- this.state = 'graceful shutdown';
- this.emit('closing');
- this.kill('SIGQUIT');
- this.pendingDeaths = this.children.length;
- };
- /**
- * Hard shutdwn, immediately kill all workers.
- *
- * @api public
- */
- Master.prototype.destroy = function(){
- this.state = 'hard shutdown';
- this.emit('closing');
- this.kill('SIGKILL');
- this._destroy();
- };
- /**
- * Attempt restart, while respecting the `restart threshold`
- * setting, to help prevent recursive restarts.
- *
- * @param {String} sig
- * @api private
- */
- Master.prototype.attemptRestart = function(sig){
- var uptime = new Date - this.startup
- , threshold = this.options['restart threshold']
- , timeout = this.options['restart timeout'];
- if (this.__restarting) return;
- if (uptime < threshold) {
- this.__restarting = true;
- this.emit('cyclic restart');
- setTimeout(function(self){
- self.restart(sig);
- }, timeout, this);
- } else {
- this.restart(sig);
- }
- };
- /**
- * Restart all workers, by sending __SIGQUIT__
- * or `sig` to them, enabling master to re-spawn.
- *
- * @param {String} sig
- * @return {ChildProcess} replacement master process
- * @api public
- */
- Master.prototype.restart = function(sig){
- var data = {}
- , proc = this.spawnMaster();
- // pass object to plugins, allowing them
- // to patch it, and utilize the data in
- // the new Master
- this.emit('restarting', data);
- proc.sock.write(utils.frame({
- method: 'connectMaster'
- , args: [sig || 'SIGQUIT']
- }), 'ascii', this.fd);
- this.on('close', function(){
- proc.sock.write(utils.frame({
- method: 'masterKilled'
- , args: [data]
- }), 'ascii');
- });
- return proc;
- };
- /**
- * Spawn a new master process.
- *
- * @return {ChildProcess}
- * @api private
- */
- Master.prototype.spawnMaster = function(){
- var fds = socketpair()
- , customFds = [fds[0], 1, 2]
- , env = {};
- // merge current env
- for (var key in process.env) {
- env[key] = process.env[key];
- }
- delete env.CLUSTER_MASTER_PID;
- env.CLUSTER_REPLACEMENT_MASTER = 1;
- env.CLUSTER_PARENT_PID = this.pid;
- // spawn new master process
- var proc = spawn(node, this.cmd, {
- customFds: customFds
- , env: env
- });
-
- // unix domain socket for ICP + fd passing
- proc.sock = new net.Socket(fds[1], 'unix');
- return proc;
- };
- /**
- * Master replacement connected.
- *
- * @param {String} sig
- * @api private
- */
- Master.prototype.connectMaster = function(sig){
- var self = this;
- function kill(){
- process.kill(self.ppid, sig);
- }
- if (this.listening) return kill();
- this.on('listening', kill);
- };
- /**
- * Original master has died aka 'retired',
- * we now fire the 'restart' event.
- *
- * @param {Object} data
- * @api private
- */
- Master.prototype.masterKilled = function(data){
- this.emit('restart', data);
- };
- /**
- * Accept fd from parent master, then `setupIPC()`.
- *
- * @api private
- */
- Master.prototype.acceptFd = function(){
- var self = this
- , stdin = new net.Socket(0, 'unix');
- // set fd and start master
- stdin.setEncoding('ascii');
- stdin.on('fd', function(fd){
- self.fd = fd;
- self.setupIPC();
- });
- // frame commands from the parent master
- stdin.on('data', this.frame.bind(this));
- stdin.resume();
- };
- /**
- * Close servers and emit 'close' before exiting.
- *
- * @api private
- */
- Master.prototype._destroy = function(){
- this.IPCSocket().close();
- if (this.fd) close(this.fd);
- this.emit('close');
- process.nextTick(process.exit.bind(process));
- };
- /**
- * Worker is connected.
- *
- * @param {Worker} worker
- * @api private
- */
- Master.prototype.connect = function(worker){
- this.emit('worker connected', worker);
- };
- /**
- * Start listening, when `shouldBind` is `true` the socket
- * will be bound, and will start listening for connections.
- *
- * @param {Boolean} shouldBind
- * @api private
- */
- Master.prototype.startListening = function(shouldBind){
- var self = this;
- // remove unix domain socket
- if ('string' == typeof this.port && shouldBind) {
- fs.unlink(this.port, function(err){
- if (err && 'ENOENT' != err.code) throw err;
- startListening();
- });
- } else {
- startListening();
- }
- // bind / listen
- function startListening() {
- if (shouldBind) {
- try {
- bind(self.fd, self.port, self.host);
- listen(self.fd, self.options.backlog);
- } catch(e) {
- self.kill('SIGKILL');
- throw e;
- }
- }
- self.callback && self.callback();
- self.emit('listening');
- }
- };
- /**
- * The given `worker` has been killed.
- * Emit the "worker killed" event, remove
- * the worker, and re-spawn depending on
- * the master state.
- *
- * @api private
- */
- Master.prototype.workerKilled = function(worker){
- // if we have many failing workers at boot
- // then we likely have a serious issue.
- if (new Date - this.startup < 20000) {
- if (++this._killed == 20) {
- console.error('');
- console.error('Cluster detected over 20 worker deaths in the first');
- console.error('20 seconds of life, there is most likely');
- console.error('a serious issue with your server.');
- console.error('');
- console.error('aborting.');
- console.error('');
- process.exit(1);
- }
- }
- // emit event
- this.emit('worker killed', worker);
- // always remove worker
- this.removeWorker(worker.id);
- // state specifics
- switch (this.state) {
- case 'hard shutdown':
- break;
- case 'graceful shutdown':
- --this.pendingDeaths || this._destroy();
- break;
- default:
- this.spawnWorker(worker.id);
- }
- };
- /**
- * `worker` received exception `err`.
- *
- * @api private
- */
- Master.prototype.workerException = function(worker, err){
- this.emit('worker exception', worker, err);
- };
- /**
- * Received worker timeout.
- *
- * @api private
- */
- Master.prototype.workerTimeout = function(worker, timeout){
- this.emit('worker timeout', worker, timeout);
- };
- /**
- * Worker waiting on `connections` to close.
- *
- * @api private
- */
- Master.prototype.workerWaiting = function(worker, connections){
- this.emit('worker waiting', worker, connections);
- };
- /**
- * Send `sig` to all worker processes, defaults to __SIGTERM__.
- *
- * @param {String} sig
- * @api public
- */
- Master.prototype.kill = function(sig){
- var self = this;
- this.emit('kill', sig);
- this.children.forEach(function(worker){
- worker.proc.kill(sig);
- });
- };
|