Permalink
Browse files

Initial release

  • Loading branch information...
rauchg committed Jan 27, 2012
0 parents commit acb716417adb5b908fc895849c1d21aadeccf7ff
Showing with 640 additions and 0 deletions.
  1. +1 −0 .gitignore
  2. +142 −0 README.md
  3. +230 −0 bin/up
  4. +30 −0 examples/hello-world/server.js
  5. +4 −0 examples/hello-world/up.js
  6. +16 −0 package.json
  7. +182 −0 up.js
  8. +35 −0 worker.js
@@ -0,0 +1 @@
+node_modules
142 README.md
@@ -0,0 +1,142 @@
+
+## Up
+
+Zero-downtime reloads built on top of the
+[distribute](http://github.com/learnboost/distribute) load balancer.
+
+### Setup
+
+Make sure you structure your code so that your `http` server lives in a
+separate module that can be `require`d.
+
+**server.js**
+```js
+module.exports = http.Server(function (req, res) {
+ res.writeHead(200);
+ res.end('Hello World');
+});
+```
+
+#### A) CLI
+
+```bash
+up -p 3000 server.js
+```
+
+`up` right now accepts three options:
+
+- `-p`/`--port`
+
+ - the port to listen on. Not required if the module already `listen`s.
+ - Defaults to `3000`.
+
+- `-w`/`--watch`
+
+ - Whether to watch for changes.
+ - Watches the directory that's active when the command is run.
+
+- `-r`/`--require` `<mod>`
+
+ - Specifies a module to require from each worker.
+ - Can be used multiple times.
+
+- `-n`/`--number`
+
+ - number of workers. It gets evaluated with
+ [eq.js](https://gist.github.com/1590954).
+ - You can optionally use the `cpus` variable. eg: `cpus + 2`.
+ - You can use all the `Math` methods. eg: `round(cpus / 2)`.
+ - Defaults to `1` in development, number of CPUs otherwise.
+
+- `-t`/`--timeout`
+
+ - number of ms after which a worker is killed once it becomes inactive.
+ - Strings like `'10s'` are accepted.
+ - Defaults to `'10m'`.
+
+#### B) JavaScript API
+
+```js
+var up = require('up')
+ , master = http.Server().listen(3000)
+
+// initialize up
+var srv = up(master, __dirname + '/server');
+
+process.on('SIGUSR2', function () {
+ srv.reload();
+});
+```
+
+`require('up')` exports the `UpServer` constructor, which takes three
+parameters:
+
+- server (`http.Server`) server to accept connections on
+- module (`String`) absolute path to the module.
+- options (`Object`)
+ - `numWorkers`: (`Number`|`String`): see `--workers` above.
+ - `workerTimeout`: (`Number`|`String`): see `--timeout` above.
+
+### Middleware
+
+An `UpServer` inherits from a `Distributor`, which means you can `use()`
+any [distribute](http://github.com/learnboost/distribute) middleware.
+
+The main difference is that the "default handler" of up (ie: the last
+function in the middleware chain) is the one that executes the
+round-robin load balancing.
+
+### Reloading
+
+To reload the workers, call `srv.reload()`. In the example above and CLI,
+this is called by sending the `SIGUSR2` signal:
+
+```bash
+$ kill -s SIGUSR2 <process id>
+```
+
+If you're running with `up` CLI, this command is output to stderr for your
+convenience.
+
+The CLI tool also auto-reloads if you pass the `--watch` option with a
+directory (which defaults to the directory of the server module).
+
+#### Strategy
+
+An up server starts with an arbitrary number of workers, which defaults to
+the number of CPUs times two.
+
+When a reload instruction is received, it spawns an identical number of
+workers.
+
+Upon the first of those workers binding to a port, any subsequent requests
+are sent to that worker, and all the ones containing old code are
+discarded.
+
+As other workers bind and become available, they join the round-robin
+round.
+
+### Credits
+
+(The MIT License)
+
+Copyright (c) 2011 Guillermo Rauch &lt;guillermo@learnboost.com&gt;
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
230 bin/up
@@ -0,0 +1,230 @@
+#!/usr/bin/env node
+
+/**
+ * Force debug.
+ */
+
+process.env.DEBUG = (process.env.DEBUG ? ',' : '') + 'up,up-cli';
+
+/**
+ * Module dependencies.
+ */
+
+var up = require('../up')
+ , spawn = require('child_process').spawn
+ , program = require('commander')
+ , http = require('http')
+ , path = require('path')
+ , os = require('os')
+ , eq = require('eq')
+ , ms = require('ms')
+ , fs = require('fs')
+ , debug = require('debug')('up-cli')
+
+/**
+ * Number of CPUs available.
+ */
+
+var cpus = require('os').cpus().length;
+
+/**
+ * Set up CLI.
+ */
+
+program
+ .version(up.version)
+ .usage('[options] <file>')
+ .option('-p, --port [port]', 'Port to listen on.', 3000)
+ .option('-w, --watch', 'Watch the module directory for changes.')
+ .option('-r, --require [file]', 'Module to require from each worker.')
+ .option('-n, --number [workers]', 'Number of workers to spawn.'
+ , 'development' == process.env.NODE_ENV ? 1 : cpus)
+ .option('-t, --timeout [ms]', 'Worker timeout.', '10ms')
+
+/**
+ * Capture requires.
+ */
+
+var requires = [];
+
+program.on('require', function (file) {
+ requires.push(file);
+});
+
+/**
+ * Parse argv.
+ */
+
+program.parse(process.argv);
+
+/**
+ * Helper function to exit with an error.
+ *
+ * @api private
+ */
+
+function error () {
+ console.error.apply(console, arguments);
+ process.exit(1);
+};
+
+/**
+ * Parse module.
+ */
+
+var file = program.args[0]
+ , server
+
+// verify a file is supplied
+if (!file) error(program.helpInformation());
+
+// absolutize
+if ('/' != file[0]) file = process.cwd() + '/' + file;
+
+// verify we can require
+try {
+ server = require(file);
+} catch (e) {
+ error('\n Error requiring supplied module "%s".\n %s\n'
+ , file, e.stack);
+}
+
+// verify it's a valid server
+if (!(server instanceof http.Server)) {
+ error('\n Module supplied "%s" does not export a valid `http.Server`.\n'
+ , file);
+}
+
+/**
+ * Parse port
+ */
+
+var port;
+
+if (null != program.port) {
+ port = Number(program.port);
+
+ if (!port || isNaN(port)) {
+ error('\n Invalid port "%s" (%d).\n', program.port, program.port);
+ }
+}
+
+/**
+ * Parse number of workers.
+ */
+
+var numWorkers = eq(program.number, { cpus: cpus });
+
+if (!numWorkers || isNaN(numWorkers)) {
+ error('\n Supplied number of workers "%s" (%s) is invalid.\n'
+ , program.number, numWorkers);
+}
+
+/**
+ * Parse timouet
+ */
+
+var workerTimeout = ms(program.timeout);
+
+if (isNaN(workerTimeout)) {
+ error('\n Supplied worker timeout "%s" (%s) is invalid.\n'
+ , program.timeout, workerTimeout);
+}
+
+/**
+ * Start!
+ */
+
+debug('starting cluster with %d workers on port %d', numWorkers, port);
+debug('`\033[97mkill -s SIGUSR2 %d\033[90m` to load new code', process.pid);
+
+var httpServer = http.Server().listen(program.port)
+ , srv = up(httpServer, file, {
+ numWorkers: numWorkers
+ , workerTimeout: workerTimeout
+ , requires: requires
+ })
+
+/**
+ * Listen on SIGUSR2 signal.
+ */
+
+process.on('SIGUSR2', function () {
+ debug('\033[97mSIGUSR2\033[90m signal detected - reloading');
+ srv.reload();
+});
+
+/**
+ * Watch.
+ */
+
+if (undefined != program.watch) {
+ // from mocha/utils - released under MIT - copyright TJ Holowaychuk
+
+ /**
+ * Ignored directories.
+ */
+
+ var ignore = ['node_modules', '.git'];
+
+ /**
+ * Ignored files.
+ */
+
+ function ignored (path) {
+ return !~ignore.indexOf(path);
+ };
+
+ /**
+ * Lookup files in the given `dir`.
+ *
+ * @return {Array}
+ * @api public
+ */
+
+ function files (dir, ret) {
+ ret = ret || [];
+
+ fs.readdirSync(dir)
+ .filter(ignored)
+ .forEach(function(p){
+ p = path.join(dir, p);
+ if (fs.statSync(p).isDirectory()) {
+ files(p, ret);
+ } else if (p.match(/\.js$/)) {
+ ret.push(p);
+ }
+ });
+
+ return ret;
+ };
+
+ /**
+ * Watch the given `files` for changes
+ * and invoke `fn(file)` on modification.
+ *
+ * @param {Array} files
+ * @param {Function} fn
+ * @api private
+ */
+
+ function watch (files, fn){
+ var options = { interval: 100 };
+ files.forEach(function (file) {
+ fs.watchFile(file, options, function (curr, prev) {
+ if (prev.mtime < curr.mtime) fn(file);
+ });
+ });
+ };
+
+ /**
+ * Watch files in the path of script to run.
+ */
+
+ debug('watching "%s" for changes', watchDir);
+
+ watch(files(process.cwd()), function (file) {
+ debug('\033[97m%s\033[90m change detected - reloading', file);
+ srv.reload();
+ });
+}
Oops, something went wrong.

0 comments on commit acb7164

Please sign in to comment.