UNPKG

the-shepherd

Version:

Control a herd of wild processes.

245 lines (218 loc) 7.99 kB
[$, Os, Fs, Handlebars, Shell, Process, { Server, Worker }, Http, Opts, Helpers, Rabbit ] = [ 'bling', 'os', 'fs', 'handlebars', 'shelljs', './process', './child', './http', './opts', './helpers', './rabbit' ].map require log = $.logger "[herd-#{$.random.string 4}]" verbose = -> if Opts.verbose then log.apply null, arguments module.exports = class Herd constructor: (opts) -> @opts = Herd.defaults(opts) @shepherdId = [Os.hostname(), @opts.admin.port].join(":") @children = [] connectSignals @ # register our process signal handlers for opts in @opts.servers for index in [0...opts.count] by 1 verbose "Creating server:", opts, index @children.push new Server opts, index for opts in @opts.workers for index in [0...opts.count] by 1 verbose "Creating worker:", opts, index @children.push new Worker opts, index Http.get "/", (req, res) -> packageFile = __dirname + "/../package.json" Helpers.readJson(packageFile).wait (err, data) -> if err then res.fail err else res.pass { name: data.name, version: data.version } Http.get "/tree", (req, res) -> Process.findTree({ pid: process.pid }).then (tree) -> res.pass Process.printTree(tree) Http.get "/stop", (req, res) => @stop("SIGTERM").then (-> res.pass "Children stopped, closing server." $.delay 300, process.exit ), res.fail Http.get "/reload", (req, res) => @restart() res.redirect 302, "/tree" r = @opts.rabbitmq if r.enabled and r.url and r.channel Rabbit = require './rabbit' do -> connection = r.url $.interval 3000, -> if r.url isnt connection Rabbit.reconnect r.url Rabbit.connect r.url my = (o) => $.extend { id: @shepherdId }, o Rabbit.subscribe r.channel, { op: "ping" }, (msg) -> Process.findTree({ pid: process.pid }).then (tree) -> Rabbit.publish r.channel, my { op: "pong", tree: tree } Rabbit.subscribe r.channel, my( op: "stop" ), (msg) => @stop("SIGTERM").then -> Rabbit.publish r.channel, my { op: "stopped" } $.delay 300, process.exit Rabbit.subscribe r.channel, my( op: "reload" ), (msg) => @restart() Rabbit.publish r.channel, my { op: "restarting" } start: (p = $.Promise()) -> try return p finally listen(@).then (=> # start the admin server log "Admin server listening on port:", @opts.admin.port fail = (msg, err) -> msg = String(msg) + String(err.stack ? err) verbose msg p.reject msg if checkConflict @opts.servers then fail "port range conflict" else writeConfig(@).then ((msg) => # write the dynamic configuration verbose msg @restart().then p.resolve, p.reject ), p.reject ), p.reject seconds = (ms) -> ms / 1000 stop: (signal, timeout=30000) -> log "Stopping all children with", signal try return p = $.Progress 1 finally for child in @children when child.process verbose "Attempting to stop child:", child.process.pid, signal try p.include child.stop signal catch err log "Error stopping child:", err.stack ? err p.reject err holder = setTimeout (-> log "Failed to stop children within #{seconds timeout} seconds." ), timeout p.finish(1).then (-> log "Fully stopped." clearTimeout holder ), (err) -> log "Failed to stop:", err restart: (from = 0, done = $.Promise()) -> # perform a careful rolling restart if from is 0 then verbose "Rolling restart starting..." try return done finally next = => @restart from + 1, done child = @children[from] switch true # if the from index is past the end when from >= @children.length verbose "Rolling restart finished." done.resolve() # if there is no such server when not child? then done.reject "invalid child index: #{from}" else verbose "Rolling restart:", from, child.restart().then next, done.reject # use opts.poolName and opts.nginx.template to render the 'upstream' block for nginx buildNginxConfig = (self) -> s = "" pools = Object.create null for child in self.children if 'poolName' of child.opts and 'port' of child (pools[child.opts.poolName] or= []).push child for upstream, servers of pools s += self.opts.nginx.template({ upstream, servers, pre: "", post: "" }) verbose "nginx configuration:\n", s s # write the nginx config to a file writeConfig = (self) -> nginx = self.opts.nginx try return p = $.Promise() finally if (not nginx.enabled) or (not nginx.config) p.resolve("Nginx configuration not enabled.") else fail = (msg, err) -> p.reject(msg + (err.stack ? err)) try verbose "Writing nginx configuration to file:", nginx.config Fs.writeFile nginx.config, buildNginxConfig(self), (err) -> if err then fail "Failed to write nginx configuration file:", err else Process.exec(nginx.reload).wait (err) -> if err then fail "Failed to reload nginx:", err else p.resolve("Nginx configuration written.") catch err then fail "writeConfig exception:", err listen = (self, p = $.Promise()) -> port = self.opts.admin.port try return p finally Process.clearCache().findOne({ ports: port }).then (owner) -> if owner? log "Killing old listener on port (#{port}): #{owner.pid}" Process.clearCache().kill(owner.pid, "SIGTERM").then -> $.delay 100, -> listen self else Http.listen(port).then p.resolve, p.reject checkConflict = (servers) -> ranges = ([server.port, server.port + server.count - 1] for server in servers) for a in ranges for b in ranges switch true when a is b then continue when a[0] <= b[0] <= a[1] or a[0] <= b[1] <= a[1] verbose "Conflict in port ranges:", a, b return true false connectSignals = (self) -> clean_exit = -> log "Exiting clean..."; process.exit 0 dirty_exit = (err) -> console.error(err); process.exit 1 # on SIGINT or SIGTERM, kill everything and die for sig in ["SIGINT", "SIGTERM"] then do (sig) -> process.on sig, -> log "Got signal:", sig self.stop("SIGKILL").then clean_exit, dirty_exit # on SIGHUP, just reload all child procs process.on "SIGHUP", -> log "Got signal: SIGHUP... reloading." self.restart() process.on "exit", (code) -> log "shepherd.on 'exit',", code collectStatus = (pids) -> $.Promise.collect (Process.find { pid } for pid in pids) # make sure a herd object has all the default configuration Herd.defaults = (opts) -> opts = $.extend Object.create(null), { servers: [] workers: [] }, opts # the above has two effects: # - allows calling without arguments # - ensures that the opts object can't be polluted by Object.prototype opts.rabbitmq = $.extend Object.create(null), { enabled: false url: "amqp://localhost:5672" channel: "shepherd" }, opts.rabbitmq opts.nginx = $.extend Object.create(null), (switch Os.platform() when 'darwin' enabled: true config: "/usr/local/etc/nginx/conf.d/shepherd.conf" reload: "launchctl stop homebrew.mxcl.nginx && launchctl start homebrew.mxcl.nginx" when 'linux' enabled: true config: "/etc/nginx/conf.d/shepherd.conf" reload: "/etc/init.d/nginx reload" else enabled: false config: null reload: "echo WARN: I don't know how to reload nginx on platform: " + Os.platform() ), opts.nginx opts.nginx.template or= """ upstream {{upstream}} { {{pre}} {{#each servers}} server 127.0.0.1:{{this.port}} weight=1; {{/each}} {{post}} keepalive 32; } """ opts.nginx.template = Handlebars.compile opts.nginx.template opts.nginx.template.inspect = (level) -> # use a mock rendering as standard output return '"' + opts.nginx.template({ upstream: "{{upstream}}", pre: "{{#each servers}}" servers: [ { port: "{{this.port}}" } ] post: "{{/each}}" }) + '"' # the http server listens for REST calls and web hooks opts.admin = $.extend Object.create(null), { enabled: true port: 9000 }, opts.admin verbose "Using configuration:", require('util').inspect(opts) return opts