the-shepherd
Version:
Control a herd of wild processes.
454 lines (434 loc) • 14.2 kB
JavaScript
// Generated by CoffeeScript 1.8.0
(function() {
var $, Fs, Handlebars, Helpers, Herd, Http, Opts, Os, Process, Rabbit, Server, Shell, Worker, log, verbose, _ref, _ref1;
_ref = ['bling', 'os', 'fs', 'handlebars', 'shelljs', './process', './child', './http', './opts', './helpers', './rabbit'].map(require), $ = _ref[0], Os = _ref[1], Fs = _ref[2], Handlebars = _ref[3], Shell = _ref[4], Process = _ref[5], (_ref1 = _ref[6], Server = _ref1.Server, Worker = _ref1.Worker), Http = _ref[7], Opts = _ref[8], Helpers = _ref[9], Rabbit = _ref[10];
log = $.logger("[herd-" + ($.random.string(4)) + "]");
verbose = function() {
if (Opts.verbose) {
return log.apply(null, arguments);
}
};
module.exports = Herd = (function() {
var buildNginxConfig, checkConflict, collectStatus, connectSignals, listen, seconds, writeConfig;
function Herd(opts) {
var index, my, r, _i, _j, _k, _l, _len, _len1, _ref2, _ref3, _ref4, _ref5;
this.opts = Herd.defaults(opts);
this.shepherdId = [Os.hostname(), this.opts.admin.port].join(":");
this.children = [];
connectSignals(this);
_ref2 = this.opts.servers;
for (_i = 0, _len = _ref2.length; _i < _len; _i++) {
opts = _ref2[_i];
for (index = _j = 0, _ref3 = opts.count; _j < _ref3; index = _j += 1) {
verbose("Creating server:", opts, index);
this.children.push(new Server(opts, index));
}
}
_ref4 = this.opts.workers;
for (_k = 0, _len1 = _ref4.length; _k < _len1; _k++) {
opts = _ref4[_k];
for (index = _l = 0, _ref5 = opts.count; _l < _ref5; index = _l += 1) {
verbose("Creating worker:", opts, index);
this.children.push(new Worker(opts, index));
}
}
Http.get("/", function(req, res) {
var packageFile;
packageFile = __dirname + "/../package.json";
return Helpers.readJson(packageFile).wait(function(err, data) {
if (err) {
return res.fail(err);
} else {
return res.pass({
name: data.name,
version: data.version
});
}
});
});
Http.get("/tree", function(req, res) {
return Process.findTree({
pid: process.pid
}).then(function(tree) {
return res.pass(Process.printTree(tree));
});
});
Http.get("/stop", (function(_this) {
return function(req, res) {
return _this.stop("SIGTERM").then((function() {
res.pass("Children stopped, closing server.");
return $.delay(300, process.exit);
}), res.fail);
};
})(this));
Http.get("/reload", (function(_this) {
return function(req, res) {
_this.restart();
return res.redirect(302, "/tree");
};
})(this));
r = this.opts.rabbitmq;
if (r.enabled && r.url && r.channel) {
Rabbit = require('./rabbit');
(function() {
var connection;
connection = r.url;
return $.interval(3000, function() {
if (r.url !== connection) {
return Rabbit.reconnect(r.url);
}
});
})();
Rabbit.connect(r.url);
my = (function(_this) {
return function(o) {
return $.extend({
id: _this.shepherdId
}, o);
};
})(this);
Rabbit.subscribe(r.channel, {
op: "ping"
}, function(msg) {
return Process.findTree({
pid: process.pid
}).then(function(tree) {
return Rabbit.publish(r.channel, my({
op: "pong",
tree: tree
}));
});
});
Rabbit.subscribe(r.channel, my({
op: "stop"
}), (function(_this) {
return function(msg) {
return _this.stop("SIGTERM").then(function() {
Rabbit.publish(r.channel, my({
op: "stopped"
}));
return $.delay(300, process.exit);
});
};
})(this));
Rabbit.subscribe(r.channel, my({
op: "reload"
}), (function(_this) {
return function(msg) {
_this.restart();
return Rabbit.publish(r.channel, my({
op: "restarting"
}));
};
})(this));
}
}
Herd.prototype.start = function(p) {
if (p == null) {
p = $.Promise();
}
try {
return p;
} finally {
listen(this).then(((function(_this) {
return function() {
var fail;
log("Admin server listening on port:", _this.opts.admin.port);
fail = function(msg, err) {
var _ref2;
msg = String(msg) + String((_ref2 = err.stack) != null ? _ref2 : err);
verbose(msg);
return p.reject(msg);
};
if (checkConflict(_this.opts.servers)) {
return fail("port range conflict");
} else {
return writeConfig(_this).then((function(msg) {
verbose(msg);
return _this.restart().then(p.resolve, p.reject);
}), p.reject);
}
};
})(this)), p.reject);
}
};
seconds = function(ms) {
return ms / 1000;
};
Herd.prototype.stop = function(signal, timeout) {
var child, err, holder, p, _i, _len, _ref2, _ref3;
if (timeout == null) {
timeout = 30000;
}
log("Stopping all children with", signal);
try {
return p = $.Progress(1);
} finally {
_ref2 = this.children;
for (_i = 0, _len = _ref2.length; _i < _len; _i++) {
child = _ref2[_i];
if (!child.process) {
continue;
}
verbose("Attempting to stop child:", child.process.pid, signal);
try {
p.include(child.stop(signal));
} catch (_error) {
err = _error;
log("Error stopping child:", (_ref3 = err.stack) != null ? _ref3 : err);
p.reject(err);
}
}
holder = setTimeout((function() {
return log("Failed to stop children within " + (seconds(timeout)) + " seconds.");
}), timeout);
p.finish(1).then((function() {
log("Fully stopped.");
return clearTimeout(holder);
}), function(err) {
return log("Failed to stop:", err);
});
}
};
Herd.prototype.restart = function(from, done) {
var child, next;
if (from == null) {
from = 0;
}
if (done == null) {
done = $.Promise();
}
if (from === 0) {
verbose("Rolling restart starting...");
}
try {
return done;
} finally {
next = (function(_this) {
return function() {
return _this.restart(from + 1, done);
};
})(this);
child = this.children[from];
switch (true) {
case from >= this.children.length:
verbose("Rolling restart finished.");
done.resolve();
break;
case child == null:
done.reject("invalid child index: " + from);
break;
default:
verbose("Rolling restart:", from, child.restart().then(next, done.reject));
}
}
};
buildNginxConfig = function(self) {
var child, pools, s, servers, upstream, _i, _len, _name, _ref2;
s = "";
pools = Object.create(null);
_ref2 = self.children;
for (_i = 0, _len = _ref2.length; _i < _len; _i++) {
child = _ref2[_i];
if ('poolName' in child.opts && 'port' in child) {
(pools[_name = child.opts.poolName] || (pools[_name] = [])).push(child);
}
}
for (upstream in pools) {
servers = pools[upstream];
s += self.opts.nginx.template({
upstream: upstream,
servers: servers,
pre: "",
post: ""
});
}
verbose("nginx configuration:\n", s);
return s;
};
writeConfig = function(self) {
var err, fail, nginx, p;
nginx = self.opts.nginx;
try {
return p = $.Promise();
} finally {
if ((!nginx.enabled) || (!nginx.config)) {
p.resolve("Nginx configuration not enabled.");
} else {
fail = function(msg, err) {
var _ref2;
return p.reject(msg + ((_ref2 = err.stack) != null ? _ref2 : err));
};
try {
verbose("Writing nginx configuration to file:", nginx.config);
Fs.writeFile(nginx.config, buildNginxConfig(self), function(err) {
if (err) {
return fail("Failed to write nginx configuration file:", err);
} else {
return Process.exec(nginx.reload).wait(function(err) {
if (err) {
return fail("Failed to reload nginx:", err);
} else {
return p.resolve("Nginx configuration written.");
}
});
}
});
} catch (_error) {
err = _error;
fail("writeConfig exception:", err);
}
}
}
};
listen = function(self, p) {
var port;
if (p == null) {
p = $.Promise();
}
port = self.opts.admin.port;
try {
return p;
} finally {
Process.clearCache().findOne({
ports: port
}).then(function(owner) {
if (owner != null) {
log("Killing old listener on port (" + port + "): " + owner.pid);
return Process.clearCache().kill(owner.pid, "SIGTERM").then(function() {
return $.delay(100, function() {
return listen(self);
});
});
} else {
return Http.listen(port).then(p.resolve, p.reject);
}
});
}
};
checkConflict = function(servers) {
var a, b, ranges, server, _i, _j, _len, _len1, _ref2, _ref3;
ranges = (function() {
var _i, _len, _results;
_results = [];
for (_i = 0, _len = servers.length; _i < _len; _i++) {
server = servers[_i];
_results.push([server.port, server.port + server.count - 1]);
}
return _results;
})();
for (_i = 0, _len = ranges.length; _i < _len; _i++) {
a = ranges[_i];
for (_j = 0, _len1 = ranges.length; _j < _len1; _j++) {
b = ranges[_j];
switch (true) {
case a === b:
continue;
case (a[0] <= (_ref2 = b[0]) && _ref2 <= a[1]) || (a[0] <= (_ref3 = b[1]) && _ref3 <= a[1]):
verbose("Conflict in port ranges:", a, b);
return true;
}
}
}
return false;
};
connectSignals = function(self) {
var clean_exit, dirty_exit, sig, _fn, _i, _len, _ref2;
clean_exit = function() {
log("Exiting clean...");
return process.exit(0);
};
dirty_exit = function(err) {
console.error(err);
return process.exit(1);
};
_ref2 = ["SIGINT", "SIGTERM"];
_fn = function(sig) {
return process.on(sig, function() {
log("Got signal:", sig);
return self.stop("SIGKILL").then(clean_exit, dirty_exit);
});
};
for (_i = 0, _len = _ref2.length; _i < _len; _i++) {
sig = _ref2[_i];
_fn(sig);
}
process.on("SIGHUP", function() {
log("Got signal: SIGHUP... reloading.");
return self.restart();
});
return process.on("exit", function(code) {
return log("shepherd.on 'exit',", code);
});
};
collectStatus = function(pids) {
var pid;
return $.Promise.collect((function() {
var _i, _len, _results;
_results = [];
for (_i = 0, _len = pids.length; _i < _len; _i++) {
pid = pids[_i];
_results.push(Process.find({
pid: pid
}));
}
return _results;
})());
};
return Herd;
})();
Herd.defaults = function(opts) {
var _base;
opts = $.extend(Object.create(null), {
servers: [],
workers: []
}, opts);
opts.rabbitmq = $.extend(Object.create(null), {
enabled: false,
url: "amqp://localhost:5672",
channel: "shepherd"
}, opts.rabbitmq);
opts.nginx = $.extend(Object.create(null), ((function() {
switch (Os.platform()) {
case 'darwin':
return {
enabled: true,
config: "/usr/local/etc/nginx/conf.d/shepherd.conf",
reload: "launchctl stop homebrew.mxcl.nginx && launchctl start homebrew.mxcl.nginx"
};
case 'linux':
return {
enabled: true,
config: "/etc/nginx/conf.d/shepherd.conf",
reload: "/etc/init.d/nginx reload"
};
default:
return {
enabled: false,
config: null,
reload: "echo WARN: I don't know how to reload nginx on platform: " + Os.platform()
};
}
})()), opts.nginx);
(_base = opts.nginx).template || (_base.template = "upstream {{upstream}} {\n {{pre}}\n {{#each servers}}\n server 127.0.0.1:{{this.port}} weight=1;\n {{/each}}\n {{post}}\n keepalive 32;\n}");
opts.nginx.template = Handlebars.compile(opts.nginx.template);
opts.nginx.template.inspect = function(level) {
return '"' + opts.nginx.template({
upstream: "{{upstream}}",
pre: "{{#each servers}}",
servers: [
{
port: "{{this.port}}"
}
],
post: "{{/each}}"
}) + '"';
};
opts.admin = $.extend(Object.create(null), {
enabled: true,
port: 9000
}, opts.admin);
verbose("Using configuration:", require('util').inspect(opts));
return opts;
};
}).call(this);