katana
Version:
Easy to use, modular web framework for any Node.js samurai.
267 lines (211 loc) • 6.08 kB
JavaScript
var Path = require('path');
var Fs = require('fs');
var EventEmitter = require('events').EventEmitter;
var cluster = require('cluster');
var root = process.cwd() + Path.sep;
var cores = require('os').cpus().length;
var options = {};
cluster.setMaxListeners(100);
cluster.__proto__.__defineGetter__('size', function() {
return Object.keys(this.workers).length;
});
module.exports = {
__proto__: EventEmitter.prototype,
options: {
script: root +'app.js',
workers: cores,
env: null,
args: [],
workon: 'online',
silent: true,
signals: true,
respawn: true,
respawnTimeout: 2000,
repl: false,
forceTimeout: 5000
},
danger: false,
restarting: false,
quitting: false,
start: function(conf, callback) {
if (typeof(conf) === 'function') {
callback = conf; conf = {};
}
this.options = options = conf;
this.set(options);
if (!Fs.existsSync(options.script)) {
console.error(' Error:', 'script doesn\'t exists!');
process.exit(1);
}
cluster.setupMaster({
exec: options.script,
env: options.env,
args: options.args,
silent: options.silent
});
options.signals && this.setupSignals();
cluster.on('fork', this.onWorkerFork.bind(this));
this.resize(callback.bind(this));
},
onWorkerFork: function(worker) {
var self = this;
console.log('Worker', worker.id, options.workon);
worker.birth = Date.now();
worker.pid = worker.process.pid;
worker.__proto__.__defineGetter__('lifetime', function() {
return (Date.now() - this.birth);
});
var timer = null;
worker.on('disconnect', function() {
timer = setTimeout(function() {
worker.process.kill('SIGKILL');
}, options.forceTimeout);
});
worker.on('exit', function() {
clearTimeout(timer);
if (!worker.suicide) {
console.log('Worker', worker.id, 'exited abnormally');
if (worker.lifetime < options.respawnTimeout) {
console.log('Worker', worker.id, 'died too quickly!');
self.danger = true;
return setTimeout(self.resize.bind(self), 2000);
}
}
(cluster.size < options.workers && options.respawn && !self.resizing && !self.restarting) && self.resize();
});
},
completeResize: function(callback) {
this.resizing = false;
if (options.workers !== cluster.size) {
if (this.danger && !options.workers) {
console.log('danger exit');
process.exit(1);
} else {
this.danger = true;
setTimeout(this.resize(this), 1000);
}
} else { this.danger = false; }
callback && callback();
},
resize: function(size, callback) {
if (typeof(size) === 'function') {
callback = size; size = options.workers;
}
if (this.resizing) { return; }
(size >= 0) && (options.workers = size);
var keys = Object.keys(cluster.workers);
var request = options.workers - keys.length;
var action = request > 0 ? options.workon : 'exit';
var count = request = request > 0 ? request : -request;
if (options.workers === keys.length) {
this.resizing = false;
return this.completeResize(callback);
}
var self = this;
function checkComplete(worker) {
if (!--request) {
self.resizing = false;
cluster.removeListener(action, checkComplete);
self.completeResize(callback);
}
}
cluster.on(action, checkComplete);
while(count--) {
if (action !== 'exit') {
cluster.fork(options.env);
} else {
var worker = cluster.workers[keys[count]];
if (worker && worker.process.connected) {
worker.disconnect();
}
}
}
},
restart: function(grace, callback) {
if (this.restarting) { return; }
if (typeof(grace) === 'function') {
callback = grace; grace = false;
}
var self = this;
var size = this.size;
self.restarting = true;
if (!grace) {
return self.resize(0, function() {
self.resize(size, function() {
self.restarting = false;
callback && callback();
});
});
}
var keys = Object.keys(cluster.workers);
function next() {
var worker = cluster.workers[keys.shift()];
worker.once('exit', function() {
cluster.fork(options.env);
});
cluster.once(options.workon, function(work) {
if (keys.length) {
next();
} else {
self.restarting = false;
callback && callback();
}
});
worker.disconnect();
}
next();
},
quit: function() {
if (this.quitting) {
console.log('forceful shutdown!');
Object.keys(cluster.workers).forEach(function(id) {
var worker = cluster.workers[id];
worker && worker.process && worker.process.kill('SIGKILL');
});
process.exit(1);
}
console.log('graceful shutdown');
this.quitting = true;
this.resize(0, function() {
console.log('cluster graceful shutdown complete');
});
},
quitHard: function() {
if (this.quitting) { return; }
this.quitting = true;
this.quit();
},
setupSignals: function() {
var self = this;
try {
process.on('SIGINT', this.quit.bind(this));
process.on('SIGHUP', this.restart.bind(this));
process.on('SIGUSR2', function() {
self.restart(true);
});
} catch(error) {
// Windows must dieeee!
}
if (process.platform === 'win32') {
var readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
readline.on('SIGINT', function() {
readline.pause();
process.emit('SIGINT');
});
}
// process.on('exit', this.quitHard.bind(this));
},
set: function(conf) {
conf = conf || {};
var key = null;
for(key in conf) {
options[key] = conf[key];
}
},
get size() {
return cluster.size;
}
}