From 4acbd3b3f8659f3d2561158f59ea3c5fd43c7b5a Mon Sep 17 00:00:00 2001 From: Stephane Alnet Date: Tue, 14 Mar 2017 11:35:31 +0100 Subject: [PATCH] add support for cluster --- docs/reference.md | 1 + package.json | 3 +- src/zappa.coffee.md | 124 ++++++++++++++++++++++++++++++++++------ tests/cluster.coffee.md | 30 ++++++++++ 4 files changed, 138 insertions(+), 20 deletions(-) create mode 100644 tests/cluster.coffee.md diff --git a/docs/reference.md b/docs/reference.md index fcb9b8d..778a754 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -114,6 +114,7 @@ You can also pass the parameters in the `options` object; they are described in * `host`: the hostname or IP address for the web server. * `path`: IPC path; if present, ZappaJS will start an [IPC server](https://nodejs.org/api/net.html#net_server_listen_path_backlog_callback) instead of a TCP/IP server. * `ready`: this function is called once the server is ready to accept requests. +* `server`: if the `server` option is set to the string `cluster`, ZappaJS will use `throng` to start and manage a Node.js cluster. In this case the function will not return anything useful; use the `ready` option to handle server startup. The default port and host may also be specified as part of the environment variables, using `ZAPPA_PORT` and `ZAPPA_HOST`, respectively. diff --git a/package.json b/package.json index 0be55b9..c8ee49e 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "socket.io": "^1.7.3", "socket.io-client": "^1.7.3", "teacup": "^2.0.0", + "throng": "^4.0.0", "uglify-js": "^2.8.4" }, "devDependencies": { @@ -73,7 +74,7 @@ ], "scripts": { "pretest": "npm install -d", - "test": "coffee tests/index.coffee && mocha --compilers coffee.md:coffee-script/register mocha/tests/", + "test": "coffee tests/index.coffee && mocha --compilers coffee.md:coffee-script/register mocha/tests/ && coffee tests/cluster.coffee.md", "prepublish": "coffee -o lib -c -M src/*.coffee.md", "docs": "docco src/*.coffee.md", "clean": "rm lib/*.js lib/*.js.map benchmarks/out/*.dat benchmarks/out/*.out tests/*.js", diff --git a/src/zappa.coffee.md b/src/zappa.coffee.md index 954dd8f..e07340b 100755 --- a/src/zappa.coffee.md +++ b/src/zappa.coffee.md @@ -44,6 +44,14 @@ Flatten array recursively (copied from Express's utils.js) ret.push o ret +Address-hash + + connection_hash = ({remoteAddress},len) -> + sum = 0 + for i in [0...remoteAddress.len] + sum += remoteAddress.charCodeAt 1 + i % len + Zappa Application ================= @@ -703,30 +711,108 @@ Takes a function and runs it as a zappa app. Optionally accepts a port number, a when 'path' then ipc_path = v else options[k] = v - zapp = zappa.app(root_function,options) - {server,app} = zapp +Listen for connections +---------------------- - server.once 'listening', -> - addr = server.address() - channel = if typeof addr is 'string' then addr else addr.address + ':' + addr.port - debug """ - Express server listening on #{channel} in #{app.settings.env} mode. - Zappa #{zappa.version} orchestrating the show. + listen = (zapp) -> - """ + {server} = zapp - if options.ready? - server.once 'listening', -> options.ready zapp + server.once 'listening', -> + addr = server.address() + channel = if typeof addr is 'string' then addr else addr.address + ':' + addr.port + debug """ + Express server listening on #{channel}, + Zappa #{zappa.version} orchestrating the show. - switch - when ipc_path - server.listen ipc_path - when host - server.listen port, host - else - server.listen port + """ + + if options.ready? + options.ready zapp + + return + + switch + when ipc_path + server.listen ipc_path + when host + server.listen port, host + else + server.listen port + +Cluster mode +------------ + +Inspired by https://github.com/elad/node-cluster-socket.io + + if options.server is 'cluster' + + throng = require 'throng' + cluster = require 'cluster' + net = require 'net' + + +Cluster master +-------------- + + master = -> + workers = [] + + cluster.on 'fork', (worker) -> + debug 'Adding worker', worker.id + workers.push worker + null + cluster.on 'exit', (worker) -> + index = workers.indexOf worker + debug 'Removing worker', worker.id, index + if index >= 0 + workers.splice index, 1 + null + + server_options = pauseOnConnect: true + + if options.https? + for own k,v of options.https + server_options[k] ?= v + + server = net.createServer server_options, (connection) -> + worker = workers[ connection_hash connection, workers.length ] + worker.send 'sticky-session:connection', connection + + zapp = {server} + listen zapp + + start = (id) -> + debug "Starting worker #{id}" + + http_module = switch + when options.http_module? + options.http_module + when options.https? + require 'https' + else + require 'http' + + server = options.server = http_module.createServer() + {app} = zappa.app root_function, options + server.on 'request', app + process.on 'message', (message, connection) -> + return unless message is 'sticky-session:connection' + server.emit 'connection', connection + connection.resume() + + throng {master,start} + zapp = null + +Non-cluster (legacy) mode +------------------------- + + else + + zapp = zappa.app root_function, options + listen zapp -The value returned by `Zappa.run` (aka `Zappa`) is the global context. +The value returned by `Zappa.run` (aka `Zappa`) is the global context in legacy mode, `null` in cluster mode. zapp diff --git a/tests/cluster.coffee.md b/tests/cluster.coffee.md new file mode 100644 index 0000000..c9691e3 --- /dev/null +++ b/tests/cluster.coffee.md @@ -0,0 +1,30 @@ + zappa = require '../src/zappa' + port = 15804 + + seem = require 'seem' + request = require 'superagent' + assert = require 'assert' + + sleep = (timeout) -> + new Promise (resolve) -> + setTimeout resolve, timeout + + do -> + + ready = seem -> + + yield sleep 1000 + + {text} = yield request.get "http://localhost:#{port}/" + assert text is 'cluster' + + {text} = yield request.get "http://127.0.0.1:#{port}/" + assert text is 'cluster' + + console.log 'Cluster test OK' + + process.exit(0) + + zappa {ready, server:'cluster', port}, -> + @get '/': 'cluster' +