UNPKG

the-shepherd

Version:

Control a herd of wild processes.

454 lines (434 loc) 14.2 kB
// 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);