├── .gitignore ├── .gitmodules ├── .npmignore ├── History.md ├── LICENSE ├── Makefile ├── Readme.md ├── docs ├── api.md ├── cli.md ├── debug.md ├── logger.md ├── pidfiles.md ├── reload.md ├── repl.md └── stats.md ├── examples ├── .gitignore ├── app-cluster.js ├── app.js ├── basic.js ├── cli-app.js ├── cli.js ├── connect.js ├── debug.js ├── envs.js ├── error.js ├── express.js ├── logger.js ├── logs │ └── .gitignore ├── pids.js ├── pids │ └── .gitignore ├── reload.js ├── repl-unix.js ├── repl.js ├── single.js ├── standalone.js ├── tcp.js ├── unix.js └── vhost.js ├── index.js ├── lib ├── cluster.js ├── master.js ├── mixins │ └── receiver.js ├── plugins │ ├── cli.js │ ├── debug.js │ ├── logger.js │ ├── pidfiles.js │ ├── reload.js │ ├── repl.js │ └── stats.js ├── utils.js └── worker.js ├── package.json └── test ├── common.js ├── logs ├── .gitignore └── nested │ └── .gitignore ├── pids └── .gitignore ├── run ├── support ├── all.js ├── exported.js ├── logs │ └── .gitignore ├── pids │ └── .gitignore ├── server.js └── standalone.js ├── test.basic.js ├── test.cli-status.js ├── test.dns.js ├── test.env.js ├── test.ephemeral.js ├── test.filename.js ├── test.logger.custom-path.js ├── test.logger.js ├── test.pidfiles.js ├── test.restart-env.js ├── test.restart.js ├── test.shutdown-all.js ├── test.shutdown.js ├── test.standalone-shutdown.js ├── test.standalone.file.js ├── test.standalone.js ├── test.standalone.restart.js ├── test.worker-kill.js ├── test.worker-quit-keep-alive.js ├── test.worker-quit.js ├── test.worker-term.js └── test.working-dir.js /.gitignore: -------------------------------------------------------------------------------- 1 | testing 2 | test.js 3 | nohup.out 4 | node_modules 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearnBoost/cluster/0d045131963e7a12ef19e94e0e35653b2dc5bd5e/.gitmodules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test.js 2 | test 3 | testing 4 | examples 5 | *.sock 6 | *.pid 7 | *.log 8 | node_modules 9 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.7.7 / 2011-10-04 3 | ================== 4 | 5 | * Fixed `repl()` bug: ensure server exists before closing it 6 | 7 | 0.7.6 / 2011-10-03 8 | ================== 9 | 10 | * Fixed standalone read from stdin (__ENOTSOCK__) regression. Closes #153 11 | 12 | 0.7.5 / 2011-09-23 13 | ================== 14 | 15 | * Fixed an `err.code` check 16 | 17 | 0.7.4 / 2011-09-19 18 | ================== 19 | 20 | * Fixed `.listen()` return value. Closes #149 [nibblebot] 21 | * Fixed `cli()` hang regression. Closes #148 22 | 23 | 0.7.3 / 2011-09-12 24 | ================== 25 | 26 | * Changed: nextTick() uncaughtException handler 27 | 28 | 0.7.2 / 2011-09-12 29 | ================== 30 | 31 | * Changed `reload()` to reload workers only 32 | * Changed: use preventDefault instead of `exit()` in `cli()` 33 | * Fixed "listening" event by deferring "start" 34 | * Removed local socket usage as it is being removed from node 35 | 36 | 0.7.1 / 2011-08-18 37 | ================== 38 | 39 | * Added vhost example. Closes #144 40 | * Fixed `cli()` plugin. Closes #145 [reported by alefnula] 41 | 42 | 0.7.0 / 2011-08-15 43 | ================== 44 | 45 | * Fixed stand-alone support with a file path. Closes #141 [reported by SebastianEdwards] 46 | * Fixed clobbering of "worker exception" [reported by fredericosilva] 47 | * Fixed `logger()` / `pidfiles()` errors when dir does not exist, now using 48 | * Removed remaining 2.x support. Closes #108 49 | mkdirp. Closes #783 50 | 51 | 0.6.9 / 2011-07-20 52 | ================== 53 | 54 | * Fixed typo in `reload()` plugin causing the `signal` option to fail. Closes #131 55 | 56 | 0.6.8 / 2011-07-19 57 | ================== 58 | 59 | * Removed unnecessary use of client socket causing `bind()` errors 60 | 61 | 0.6.7 / 2011-07-19 62 | ================== 63 | 64 | * Added test case for #125 [felixge] 65 | * Expose child `Worker` instance `.worker` 66 | * Fixed restart race-condition. Closes #125 67 | 68 | 0.6.6 / 2011-07-18 69 | ================== 70 | 71 | * Added stand-alone restart test 72 | * Added stand-alone shutdown test 73 | * Added simple stand-alone test 74 | * Removed 0.2.x compatibility 75 | * Changed: sync unlink of server / client sockets 76 | * Fixed stand-alone issue with not killing the parent master. Closes #565 77 | 78 | 0.6.5 / 2011-07-18 79 | ================== 80 | 81 | * Added support for calling any master method from a worker [felixge] 82 | * Changed internal IPC to use UDP. Closes #126 83 | * Fixed: __SIGKILL__ children on master uncaught exception 84 | 85 | 0.6.4 / 2011-06-14 86 | ================== 87 | 88 | * Fix for json framing. Closes #109 89 | 90 | 0.6.3 / 2011-06-11 91 | ================== 92 | 93 | * Added `{ color: false }` option to `debug()` 94 | * Fixed; close socketpair fds when worker dies 95 | * Fixed `Master#listen()` with env specific config. Closes #98 96 | 97 | 0.6.2 / 2011-05-11 98 | ================== 99 | 100 | * Fixed IPC for workers without a server. Closes #91 101 | * Fixed `close(fd)` issue for Master without a server. Closes #89 102 | 103 | 0.6.1 / 2011-04-26 104 | ================== 105 | 106 | * Changed; cli commands will now signal orphaned children 107 | * Changed; postpone spawning until "listening" this _should_ fix our EINVAL issue 108 | * Changed; exit > 0 when trying to use the `cli()` when cluster is not running 109 | * Changed; `cli()` will still operate on orphans 110 | 111 | 0.6.0 / 2011-04-18 112 | ================== 113 | 114 | * Added support to run cluster without a server. Closes #72 115 | * Renamed titles to "cluster" and "cluster worker". closes #82 116 | 117 | 0.5.7 / 2011-04-17 118 | ================== 119 | 120 | * Added `lightRequests` option to `stats()` 121 | 122 | 0.5.6 / 2011-04-15 123 | ================== 124 | 125 | * Added; expose utils, helpful for plugins 126 | * Added; default both `Master#spawn()` and `Master#remove()` to 1 127 | 128 | 0.5.5 / 2011-04-05 129 | ================== 130 | 131 | * Revert "Changed; demote user/group in master" 132 | 133 | 0.5.4 / 2011-04-05 134 | ================== 135 | 136 | * Added `title` and `worker title` settings. Closes #54 137 | * Added `request complete` `stats()` event 138 | * Changed; demote user/group in master 139 | 140 | 0.5.3 / 2011-03-30 141 | ================== 142 | 143 | * Added support for changing watched file extensions [Eiríkur Nilsson] 144 | * Fixed; reload() using extname() instead of indexOf() [reported by solsys] 145 | 146 | 0.5.1 / 2011-03-24 147 | ================== 148 | 149 | * Changed; only caught uncaughtExceptions when no other listeners are present 150 | 151 | 0.5.0 / 2011-03-24 152 | ================== 153 | 154 | * Added `connections` option to `stats()` plugin. 155 | Reports connections and disconnections, displaying in the REPL. 156 | * Added `requests` option to `stats()` plugin. 157 | Reports request statistics, displaying in the REPL. 158 | * Added support for plugins to work within workers. Closes #27 159 | * Fixed json framing race-condition. Closes #64 160 | 161 | 0.4.2 / 2011-03-15 162 | ================== 163 | 164 | * Fixed `user` / `group` options. Closes #60 165 | * Fixed; abort on many immediate worker deaths within boot 166 | * Fixed `cli()` exit when working with `reload()` (or anything else keeping the event loop active) 167 | 168 | 0.4.1 / 2011-03-10 169 | ================== 170 | 171 | * Added cyclic restart timeouts. Closes #23 172 | * Remove master __SIGHUP__ as restart 173 | 174 | 0.4.0 / 2011-03-08 175 | ================== 176 | 177 | * Added `worker removed` event 178 | * Added `spawn(-n, signal)` support defaulting to __SIGQUIT__ 179 | * Added `spawn(-n)` support. Closes #46 180 | 181 | 0.3.3 / 2011-03-03 182 | ================== 183 | 184 | * Added __CLUSTER_WORKER___ env var with the workers id 185 | 186 | 0.3.2 / 2011-03-01 187 | ================== 188 | 189 | * Fixed bug when using `cluster(filename)`, previously still requiring for master 190 | 191 | 0.3.1 / 2011-02-28 192 | ================== 193 | 194 | * Added `cluster(filename)` support. Closes #45 195 | This is highly recommended, view the API docs 196 | on the site for more info. 197 | 198 | 0.3.0 / 2011-02-28 199 | ================== 200 | 201 | * Added "worker exception" event. Closes #41 202 | * Added `listen()` host dns resolution. Closes #35 203 | * Added `pidfiles()` helper `master.pidof(name)` 204 | * Added; `reload()` ignoring _node_modules_ and similar dirs. Closes #31 205 | * Fixed master __PPID__ reference. Closes #38 206 | * Fixed restart __SIGQUIT__ default 207 | * Fixed; using `-g` for graceful shutdown instead of duplicate `-s`. Closes #39 208 | 209 | 0.2.4 / 2011-02-25 210 | ================== 211 | 212 | * Added `Master#preventDefault` support to clean `cli()`. 213 | Plugins can now tell master to "prevent its default behaviour", aka 214 | listening for connections. 215 | 216 | * Fixed bug preventing consistent envs. Closes #37 [reported by sambarnes] 217 | This caused `require.paths` to be altered. 218 | 219 | * Fixed; throw `pidfiles()` related errors, instead of ignoring 220 | 221 | 0.2.3 / 2011-02-21 222 | ================== 223 | 224 | * Fixed `reload()` plugin; protect against cyclic restarts. 225 | 226 | 0.2.2 / 2011-02-21 227 | ================== 228 | 229 | * Added __SIGCHLD__ trap to notify master of killed worker. 230 | This means that master can now recover a child that 231 | is __KILL__ed. 232 | * Removed `Master#workerKilled()` call from worker 233 | 234 | 0.2.1 / 2011-02-21 235 | ================== 236 | 237 | * Added `Master#do()` 238 | 239 | 0.2.0 / 2011-02-21 240 | ================== 241 | 242 | * Added; maintaining worker count on __SIGCHLD__. Closes #28 243 | * Added; defaulting `reload()` to the servers root dir 244 | * Changed; `reload()` filtering out non-js files. Closes #30 245 | * Removed __SIGHUP__ trap from worker 246 | 247 | 0.1.1 / 2011-02-18 248 | ================== 249 | 250 | * Added vhost example 251 | * Added restarts stat 252 | * Added `'all'` env support, `in('all')` executing regardless 253 | of the environment. Useful when `listen()`ing on the same port 254 | regardless. 255 | 256 | * Changed; `working directory` setting defaulting to the script directory (POLS) 257 | 258 | 0.1.0 / 2011-02-18 259 | ================== 260 | 261 | * Added TCP echo server example 262 | * Added REPL `shutdown()` function 263 | * Added REPL `stop()` function 264 | * Added master spawning strategy 265 | On restart, master now spawns a new master to accept 266 | connections while the previous works (and master) finish 267 | and die off. 268 | * Added `Master#in()` for environment based usage. Closes #22 269 | For example: 270 | cluster(server) 271 | .in('development') 272 | .use(cluster.debug()) 273 | .use(cluster.repl()) 274 | .listen(3000) 275 | .in('production') 276 | .use(cluster.logger()) 277 | .listen(80); 278 | 279 | * Fixed some test race-conditions 280 | * Fixed event leak. Closes #18 281 | 282 | 0.0.4 / 2011-02-17 283 | ================== 284 | 285 | * Fixed `stats()` / `repl()` breakage when used with 0.2.x due to os mod. Closes #16 286 | * Changed; close _REPL_ connections on shutdown 287 | 288 | 0.0.3 / 2011-02-16 289 | ================== 290 | 291 | * Added log dependency to _package.json_. Closes #14 292 | 293 | 0.0.2 / 2011-02-15 294 | ================== 295 | 296 | * Fixed `process.setuid()` typo 297 | 298 | 0.0.1 / 2011-02-15 299 | ================== 300 | 301 | * Initial commit -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2011 LearnBoost 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | @./test/run 4 | 5 | test-debug: 6 | @./test/run debug 7 | 8 | .PHONY: test test-debug -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Cluster 2 | 3 | [Cluster](http://learnboost.github.com/cluster) is an extensible multi-core server manager for [node.js](http://nodejs.org). 4 | 5 | ## Installation 6 | 7 | ```bash 8 | $ npm install cluster 9 | ``` 10 | 11 | ## Features 12 | 13 | - zero-downtime restart 14 | - hard shutdown support 15 | - graceful shutdown support 16 | - resuscitates workers 17 | - maintains worker count, even if worker was _SIGKILL_ed. 18 | - workers commit suicide when master dies 19 | - spawns one worker per cpu (by default) 20 | - extensible via plugins 21 | - bundled plugins 22 | - [cli](http://learnboost.github.com/cluster/docs/cli.html): provides a command-line interface for your cluster 23 | - [debug](http://learnboost.github.com/cluster/docs/debug.html): verbose debugging information 24 | - [logger](http://learnboost.github.com/cluster/docs/logger.html): master / worker logs 25 | - [pidfiles](http://learnboost.github.com/cluster/docs/pidfiles.html): writes master / worker pidfiles 26 | - [reload](http://learnboost.github.com/cluster/docs/reload.html): reloads workers when files change 27 | - [repl](http://learnboost.github.com/cluster/docs/repl.html): perform real-time administration 28 | - [stats](http://learnboost.github.com/cluster/docs/stats.html): adds real-time statistics to the `repl` plugin 29 | - supports node 0.2.x 30 | - supports node 0.4.x 31 | - supports TCP servers 32 | 33 | ## Example 34 | 35 | app.js: 36 | 37 | ```javascript 38 | var http = require('http'); 39 | 40 | module.exports = http.createServer(function(req, res){ 41 | console.log('%s %s', req.method, req.url); 42 | var body = 'Hello World'; 43 | res.writeHead(200, { 'Content-Length': body.length }); 44 | res.end(body); 45 | }); 46 | ``` 47 | 48 | server.js: 49 | 50 | ```javascript 51 | var cluster = require('cluster') 52 | , app = require('./app'); 53 | 54 | cluster(app) 55 | .use(cluster.logger('logs')) 56 | .use(cluster.stats()) 57 | .use(cluster.pidfiles('pids')) 58 | .use(cluster.cli()) 59 | .use(cluster.repl(8888)) 60 | .listen(3000); 61 | ``` 62 | 63 | Note that cluster does _not_ create these directories for you, so you may want to: 64 | 65 | $ mkdir {logs,pids} 66 | 67 | recommended usage: passing the path to prevent unnecessary database connections in the master process, as `./app` is only `require()`ed within the workers. 68 | 69 | ```javascript 70 | var cluster = require('cluster'); 71 | 72 | cluster('./app') 73 | .use(cluster.logger('logs')) 74 | .use(cluster.stats()) 75 | .use(cluster.pidfiles('pids')) 76 | .use(cluster.cli()) 77 | .use(cluster.repl(8888)) 78 | .listen(3000); 79 | ``` 80 | 81 | ## Plugins 82 | 83 | Below are the known 3rd-party plugins for cluster: 84 | 85 | - [cluster-log](https://github.com/LearnBoost/cluster-log) remote logger powered by redis 86 | - [cluster-mail](https://github.com/LearnBoost/cluster-mail) email exception notifications 87 | - [cluster-exception](https://github.com/3rd-eden/cluster.exception) extensive exception notifications 88 | - [cluster-responsetimes](https://github.com/mnutt/cluster-responsetimes) response time statistics for cluster's REPL 89 | - [cluster-vhost](https://github.com/AndreasMadsen/cluster-vhost) add virtual host support to cluster 90 | 91 | ## Screencasts 92 | 93 | - Cluster [Introduction](http://screenr.com/X8v) 94 | 95 | ## Running Tests 96 | 97 | Install development dependencies: 98 | 99 | ```bash 100 | $ npm install 101 | ``` 102 | 103 | Then: 104 | 105 | ```bash 106 | $ make test 107 | ``` 108 | 109 | Actively tested with node: 110 | 111 | - 0.4.9 112 | 113 | ## Authors 114 | 115 | * TJ Holowaychuk 116 | 117 | ## License 118 | 119 | (The MIT License) 120 | 121 | Copyright (c) 2011 LearnBoost <dev@learnboost.com> 122 | 123 | Permission is hereby granted, free of charge, to any person obtaining 124 | a copy of this software and associated documentation files (the 125 | 'Software'), to deal in the Software without restriction, including 126 | without limitation the rights to use, copy, modify, merge, publish, 127 | distribute, sublicense, and/or sell copies of the Software, and to 128 | permit persons to whom the Software is furnished to do so, subject to 129 | the following conditions: 130 | 131 | The above copyright notice and this permission notice shall be 132 | included in all copies or substantial portions of the Software. 133 | 134 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 135 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 136 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 137 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 138 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 139 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 140 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | ## API 2 | 3 | The Cluster API at its core is extremely simple, all we need to do is pass 4 | our tcp or http `server` to `cluster()`, then call `listen()` as we would on the `http.Server` itself. 5 | 6 | 7 | var cluster = require('../') 8 | , http = require('http'); 9 | 10 | var server = http.createServer(function(req, res){ 11 | res.writeHead(200); 12 | res.end('Hello World'); 13 | }); 14 | 15 | cluster(server) 16 | .listen(3000); 17 | 18 | Alternatively (and recommended) is to export your server instance via `module.exports`, and supply a path to `cluster()`. For example _app.js_: 19 | 20 | module.exports = http.createServer(....); 21 | 22 | and _server.js_ with our cluster logic, allowing our server to be `require()`ed within tests, and preventing potential issues by having open database connections etc within the master processes, as only the workers need access to the `server` instance. 23 | 24 | cluster('app') 25 | .listen(3000); 26 | 27 | A good example if this, is a long-lived database connection. Our _app.js_ may have this initialized at the top, which although will work fine stand-alone, may cause cluster's master processes to hang when restarting or closing due to the connection remaining active in the event loop. 28 | 29 | var db = redis.createClient(); 30 | 31 | ### Abstract Clusters 32 | 33 | Cluster is not bound to servers, cluster can be used to manage processes for processing job queues etc. Below is a minimalist example of this, simply invokes `cluster()` with no object, spawning a worker per cpu: 34 | 35 | var cluster = require('cluster'); 36 | 37 | var proc = cluster().start(); 38 | 39 | if (proc.isWorker) { 40 | // do things within the worker processes 41 | } else { 42 | // do something within the master 43 | } 44 | 45 | ### Plugins 46 | 47 | A plugin is simply a function that accepts the `master` process. Most plugin functions _return_ another anonymous function, allowing them to accept options, for example: 48 | 49 | function myPlugin(path){ 50 | return function(master) { 51 | // do stuff 52 | } 53 | } 54 | 55 | To use them, all we need to do is pass it to the `use()` method: 56 | 57 | cluster(server) 58 | .use(myPlugin('/some/path')) 59 | .listen(3000); 60 | 61 | To use a plugin that is bundled with Cluster simply grab it from the `cluster` object: 62 | 63 | cluster(server) 64 | .use(cluster.logger()) 65 | .listen(3000); 66 | 67 | ### Settings 68 | 69 | Below are the settings available: 70 | 71 | - `workers` Number of workers to spawn, defaults to the number of CPUs or `1` 72 | - `working directory` Working directory defaulting to the script's dir 73 | - `backlog` Connection backlog, defaulting to 128 74 | - `socket port` Master socket port defaulting to `8989` 75 | - `timeout` Worker shutdown timeout in milliseconds, defaulting to `60000` 76 | - `title` master process title defaulting to "cluster" 77 | - `worker title` worker process title defaulting to "cluster worker" 78 | - `user` User id / name 79 | - `group` Group id / name 80 | 81 | We can take what we have now, and go on to apply settings using the `set(option, value)` method. For example: 82 | 83 | cluster(server) 84 | .set('working directory', '/') 85 | .set('workers', 5) 86 | .listen(3000); 87 | 88 | ### Signals 89 | 90 | Cluster performs the following actions when handling signals: 91 | 92 | - `SIGINT` hard shutdown 93 | - `SIGTERM` hard shutdown 94 | - `SIGQUIT` graceful shutdown 95 | - `SIGUSR2` restart workers 96 | 97 | ### Events 98 | 99 | The following events are emitted, useful for plugins or general purpose logging etc. 100 | 101 | - `start`. When the IPC server is prepped 102 | - `worker`. When a worker is spawned, passing the `worker` 103 | - `listening`. When the server is listening for connections 104 | - `closing`. When master is shutting down 105 | - `close`. When master has completed shutting down 106 | - `worker killed`. When a worker has died 107 | - `worker exception`. Worker uncaughtException. Receives the worker and exception object 108 | - `kill`. When a `signal` is being sent to all workers 109 | - `restarting`. Restart requested by REPL or signal. Receives an object 110 | which can be patched in order to preserve plugin state. 111 | - `restart`. Restart complete, new master established, previous killed. 112 | Receives an object with state preserved by the `restarting` even, 113 | patched in the previous master. 114 | 115 | ### Master#state 116 | 117 | Current state of the master process, one of: 118 | 119 | - `active` 120 | - `hard shutdown` 121 | - `graceful shutdown` 122 | 123 | ### Master#isWorker 124 | 125 | `true` when the script is executed as a worker. 126 | 127 | cluster = cluster(server).listen(3000); 128 | 129 | if (cluster.isWorker) { 130 | // do something 131 | } 132 | 133 | Alternatively we can use the __CLUSTER_WORKER__ env var, populated with 134 | the worker's id. 135 | 136 | ### Master#isMaster 137 | 138 | `true` when the script is executed as master. 139 | 140 | cluster = cluster(server).listen(3000); 141 | 142 | if (cluster.isMaster) { 143 | // do something 144 | } 145 | 146 | ### Master#set(option, value) 147 | 148 | Set `option` to `value`. 149 | 150 | ### Master#use(plugin) 151 | 152 | Register a `plugin` for use. 153 | 154 | ### Master#in(env) 155 | 156 | Conditionally perform the following action, if 157 | __NODE_ENV__ matches `env`. 158 | 159 | cluster(server) 160 | .in('development').use(cluster.debug()) 161 | .in('development').listen(3000) 162 | .in('production').listen(80); 163 | 164 | The environment conditionals may be applied to several calls: 165 | 166 | cluster(server) 167 | .set('working directory', '/') 168 | .in('development') 169 | .set('workers', 1) 170 | .use(cluster.logger('logs', 'debug')) 171 | .use(cluster.debug()) 172 | .listen(3000) 173 | .in('production') 174 | .set('workers', 4) 175 | .use(cluster.logger()) 176 | .use(cluster.pidfiles()) 177 | .listen(80); 178 | 179 | If we perform the same action for environments, set them before 180 | the first `in()` call, or use `in('all')`. 181 | 182 | cluster(server) 183 | .set('working directory', '/') 184 | .do(function(){ 185 | console.log('some arbitrary action'); 186 | }) 187 | .in('development') 188 | .set('workers', 1) 189 | .use(cluster.logger('logs', 'debug')) 190 | .use(cluster.debug()) 191 | .in('production') 192 | .set('workers', 4) 193 | .use(cluster.logger()) 194 | .use(cluster.pidfiles()) 195 | .in('all') 196 | .listen(80); 197 | 198 | ### Master#spawn(n) 199 | 200 | Spawn `n` additional workers. 201 | 202 | ### Master#close() 203 | 204 | Graceful shutdown, waits for all workers to reply before exiting. 205 | 206 | ### Master#destroy() 207 | 208 | Hard shutdown, immediately kill all workers. 209 | 210 | ### Master#restart([signal]) 211 | 212 | Defaults to a graceful restart, spawning a new master process, and sending __SIGQUIT__ to the previous master process. Alternatively a custom `signal` may be passed. 213 | 214 | ### Master#kill([signal]) 215 | 216 | Sends __SIGTERM__ or `signal` to all worker processes. This method is used by `Master#restart()`, `Master#close()` etc. -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | 2 | ## CLI 3 | 4 | Adds a command-line interface to your cluster. 5 | 6 | ### Usage 7 | 8 | This plugin requires that you use `pidfiles()` 9 | above `cli()`, so that the pidfile directory 10 | is exposed. 11 | 12 | cluster(server) 13 | .use(cluster.pidfiles()) 14 | .use(cluster.cli()) 15 | .listen(3000); 16 | 17 | Once set up our server script serves as both 18 | the master, and the CLI. For example we may 19 | still launch the server(s) as shown below. 20 | 21 | $ nohup node server.js & 22 | 23 | However now we may also utilize commands 24 | provided by this plugin. 25 | 26 | $ node server.js status 27 | 28 | master 3281 alive 29 | worker 0 3282 dead 30 | worker 1 3283 alive 31 | worker 2 3284 alive 32 | worker 3 3285 alive 33 | 34 | $ node server.js restart 35 | $ node server.js shutdown 36 | 37 | For more command information use `--help`. 38 | 39 | $ node server.js --help 40 | 41 | ### Defining CLI Commands 42 | 43 | Plugins may define additional commands, simply by invoking `cluster.cli.define()` passing the flag(s), a callback function, 44 | and a description. Below is the implementation of `--help` for reference: 45 | 46 | var cli = require('cluster').cli; 47 | 48 | cli.define('-h, --help, help', function(master){ 49 | console.log('\n Usage: node \n'); 50 | commands.forEach(function(command){ 51 | console.log(' ' 52 | + command.flags.join(', ') 53 | + '\n ' 54 | + '\033[90m' + command.desc + '\033[0m' 55 | + '\n'); 56 | }); 57 | console.log(); 58 | process.exit(); 59 | }, 'Show help information'); 60 | -------------------------------------------------------------------------------- /docs/debug.md: -------------------------------------------------------------------------------- 1 | 2 | ## Debug 3 | 4 | Outputs verbose debugging information to _stderr_. 5 | 6 | info - master started 7 | info - worker 0 spawned 8 | info - worker 1 spawned 9 | info - worker 2 spawned 10 | info - worker 3 spawned 11 | info - worker 2 connected 12 | info - worker 0 connected 13 | info - worker 3 connected 14 | info - worker 1 connected 15 | info - listening for connections 16 | ^C info - shutting down 17 | warning - kill(SIGKILL) 18 | info - shutdown complete 19 | warning - worker 2 died 20 | warning - worker 0 died 21 | warning - worker 3 died 22 | 23 | ## Usage 24 | 25 | cluster(server) 26 | .use(cluster.debug()) 27 | .listen(3000); 28 | 29 | ### Options 30 | 31 | - `colors` enable color output, defaults to true -------------------------------------------------------------------------------- /docs/logger.md: -------------------------------------------------------------------------------- 1 | 2 | ## Logger 3 | 4 | File-based logging of both the _master_ and _worker_ processes. 5 | 6 | ### Usage 7 | 8 | The `logger([path[, level]])` plugin accepts an optional `path`, and optional `level` to control the verbosity of the master process logs. By default the log level is _info_. 9 | 10 | Outputting to `./logs`: 11 | 12 | cluster(server) 13 | .use(cluster.logger()) 14 | .listen(3000); 15 | 16 | 17 | Outputting to `./tmp/logs`: 18 | 19 | cluster(server) 20 | .use(cluster.logger('tmp/logs')) 21 | .listen(3000); 22 | 23 | Outputting to `/var/log/node` with a log level of `debug`: 24 | 25 | cluster(server) 26 | .use(cluster.logger('/var/log/node', 'debug')) 27 | .listen(3000); 28 | 29 | Generated files: 30 | 31 | master.log 32 | workers.access.log 33 | workers.error.log -------------------------------------------------------------------------------- /docs/pidfiles.md: -------------------------------------------------------------------------------- 1 | 2 | ## PID Files 3 | 4 | Saves out PID files, for example: 5 | 6 | master.pid 7 | worker.0.pid 8 | worker.1.pid 9 | worker.2.pid 10 | worker.3.pid 11 | 12 | ### Usage 13 | 14 | The `pidfiles([path])` plugin saves pid (process-id) files to the given `path` or `./pids`. 15 | 16 | save to `./pids`: 17 | 18 | cluster(server) 19 | .use(cluster.pidfiles()) 20 | .listen(3000); 21 | 22 | save to `/var/run/node`: 23 | 24 | cluster(server) 25 | .use(cluster.pidfiles('/var/run/node')) 26 | .listen(3000); 27 | 28 | ### master.pidfiles 29 | 30 | The pidfiles directory. 31 | 32 | ### master.pidof(name) 33 | 34 | Return a __PID__ for the given `name`. 35 | 36 | master.pidof('master') 37 | // => 5978 38 | 39 | master.pidof('worker.0') 40 | // => 5979 41 | -------------------------------------------------------------------------------- /docs/reload.md: -------------------------------------------------------------------------------- 1 | ## Reload 2 | 3 | Restart the server the given js `files` have changed. 4 | `files` may be several directories, filenames, etc, defaulting 5 | to the script's directory. 6 | 7 | ### Options 8 | 9 | - `signal` Signal to send, defaults to __SIGTERM__ 10 | - `interval` Watcher interval, defaulting to `100` 11 | 12 | ### Usage 13 | 14 | The `reload(paths[, signal])` plugin accepts a single path, or an array of paths, watching for __mtime__ changes, and re-loading the workers when a change has been made. By default the __SIGTERM__ signal is sent, killing the workers immediately, however we may pass a `signal` for graceful termination as well. 15 | 16 | Reload when files in `./` (`__dirname`) change: 17 | 18 | cluster(server) 19 | .use(cluster.reload()) 20 | .listen(3000); 21 | 22 | Reload when files in `./lib` change: 23 | 24 | cluster(server) 25 | .use(cluster.reload('lib')) 26 | .listen(3000); 27 | 28 | Reload when files in `./lib`, `./tests`, or the `./index.js` file change: 29 | 30 | cluster(server) 31 | .use(cluster.reload(['lib', 'tests', 'index.js'])) 32 | .listen(3000); 33 | 34 | Graceful shutdown: 35 | 36 | cluster(server) 37 | .use(cluster.reload('lib', { signal: 'SIGQUIT' })) 38 | .listen(3000); 39 | 40 | Watching coffee-script files as well. 41 | 42 | cluster(server) 43 | .use(cluster.reload('lib', { extensions: ['.js', '.coffee'] })) 44 | .listen(3000); -------------------------------------------------------------------------------- /docs/repl.md: -------------------------------------------------------------------------------- 1 | 2 | ## REPL 3 | 4 | Provides live administration tools for inspecting state, spawning and killing workers, and more. The __REPL__ plugin itself is extensible, for example the `stats()` plugin provides a __REPL__ function named `stats()`. 5 | 6 | ### Usage 7 | 8 | The `repl([port | path])` accepts a `port` or unix domain socket `path`, after which you may telnet to at any time. 9 | 10 | Launch the __REPL__ with a local socket: 11 | 12 | cluster(server) 13 | .use(cluster.repl('/var/run/cluster.sock')) 14 | .listen(3000); 15 | 16 | Start a telnet session: 17 | 18 | $ telnet /var/run/cluster.sock 19 | 20 | cluster> help() 21 | 22 | Commands 23 | help(): Display help information 24 | spawn(n): Spawn one or more additional workers 25 | pids(): Output process ids 26 | kill(id, signal): Send signal or SIGTERM to the given worker 27 | shutdown(): Gracefully shutdown server 28 | stop(): Hard shutdown 29 | restart(): Gracefully restart all workers 30 | echo(msg): echo the given message 31 | stats(): Display server statistics 32 | 33 | __NOTE__: a local socket is recommended, otherwise this may be a secure hole. 34 | 35 | ### pids() 36 | 37 | Outputs the master / worker process ids. 38 | 39 | cluster> pids() 40 | 41 | pids 42 | master: 1799 43 | worker #0: 1801 44 | worker #1: 1802 45 | worker #2: 1803 46 | worker #3: 1804 47 | 48 | ### spawn() 49 | 50 | Spawn an additional worker. 51 | 52 | cluster> spawn() 53 | spawning 1 worker 54 | cluster> pids() 55 | 56 | pids 57 | master: 1799 58 | worker #0: 1801 59 | worker #1: 1802 60 | worker #2: 1803 61 | worker #3: 1804 62 | worker #4: 1809 63 | 64 | ### spawn(n) 65 | 66 | Spawn `n` workers: 67 | 68 | cluster> spawn(4) 69 | spawning 4 workers 70 | cluster> pids() 71 | 72 | pids 73 | master: 1817 74 | worker #0: 1818 75 | worker #1: 1819 76 | worker #2: 1820 77 | worker #3: 1821 78 | worker #4: 1825 79 | worker #5: 1826 80 | worker #6: 1827 81 | worker #7: 1828 82 | 83 | ### kill(id[, signal]) 84 | 85 | Kill worker `id` with the given `signal` or __SIGTERM__. For graceful termination use __SIGQUIT__. 86 | 87 | cluster> pids() 88 | 89 | pids 90 | master: 1835 91 | worker #0: 1837 92 | worker #1: 1838 93 | worker #2: 1839 94 | worker #3: 1840 95 | 96 | cluster> kill(2) 97 | sent SIGTERM to worker #2 98 | cluster> kill(3) 99 | sent SIGTERM to worker #3 100 | cluster> pids() 101 | 102 | pids 103 | master: 1835 104 | worker #0: 1837 105 | worker #1: 1838 106 | worker #2: 1843 107 | worker #3: 1844 108 | 109 | ### restart() 110 | 111 | Gracefully restart all workers. 112 | 113 | cluster> pids() 114 | 115 | pids 116 | master: 1835 117 | worker #0: 1837 118 | worker #1: 1838 119 | worker #2: 1843 120 | worker #3: 1844 121 | 122 | cluster> restart() 123 | restarting 4 workers 124 | cluster> pids() 125 | 126 | pids 127 | master: 1835 128 | worker #0: 1845 129 | worker #1: 1849 130 | worker #2: 1848 131 | worker #3: 1847 132 | 133 | ### Defining REPL Functions 134 | 135 | To define a function accessible to the __REPL__, all we need to do is call `cluster.repl.define()`, passing the function, as well as a description string. 136 | 137 | Below we define the `echo()` function, simply printing the input `msg` given. As you can see our function receivers the `Master` instance, the __REPL__ `sock`, and any arguments that were passed. For example `echo("test")` would pass the `msg` as `"test"`, and `echo("foo", "bar")` would pass `msg` as `"foo"`, and `arguments[3]` as `"bar"`. 138 | 139 | repl.define('echo', function(master, sock, msg){ 140 | sock.write(msg + '\n'); 141 | }, 'echo the given message'); 142 | 143 | Shown below is a more complete example. 144 | 145 | var cluster = require('../') 146 | , repl = cluster.repl 147 | , http = require('http'); 148 | 149 | var server = http.createServer(function(req, res){ 150 | var body = 'Hello World'; 151 | res.writeHead(200, { 'Content-Length': body.length }); 152 | res.end(body); 153 | }); 154 | 155 | // custom repl function 156 | 157 | repl.define('echo', function(master, sock, msg){ 158 | sock.write(msg + '\n'); 159 | }, 'echo the given message'); 160 | 161 | // $ telnet localhots 8888 162 | 163 | cluster(server) 164 | .use(repl(8888)) 165 | .listen(3000); -------------------------------------------------------------------------------- /docs/stats.md: -------------------------------------------------------------------------------- 1 | 2 | ## Stats 3 | 4 | The stats plugin collects statistics from the events emitter by the master process, and exposes a `stats()` __REPL__ function. 5 | 6 | 7 | ### Usage 8 | 9 | To utilize simply `use()` both the `stats()` and `repl()` plugins. 10 | 11 | cluster(server) 12 | .use(cluster.stats()) 13 | .use(cluster.repl(8888)) 14 | .listen(3000); 15 | 16 | Telnet to the repl: 17 | 18 | $ telnet localhost 8888 19 | 20 | ### stats() 21 | 22 | After manually killing two workers, the stats below show information regarding system load average, uptime, total workers spawned, deaths, worker-specific stats and more. 23 | 24 | cluster> stats() 25 | 26 | Master 27 | os: Darwin 10.5.0 28 | state: active 29 | started: Fri, 11 Feb 2011 16:58:48 GMT 30 | uptime: 2 minutes 31 | workers: 4 32 | deaths: 2 33 | 34 | Resources 35 | load average: 0.35 0.23 0.15 36 | cores utilized: 4 / 4 37 | memory at boot (free / total): 2.18gb / 4.00gb 38 | memory now (free / total): 2.08gb / 4.00gb 39 | 40 | Workers 41 | 0: 2 minutes 42 | 1: 2 minutes 43 | 2: 1 minute 44 | 3: 22 seconds 45 | 46 | ### Options 47 | 48 | - `connections` enable connection statistics 49 | - `requests` enable request statistics 50 | 51 | ### Connection Statistics 52 | 53 | Cluster can report on connections made to the server in each worker. To utilize simply pass `{ connections: true }`, and then view the stats in the REPL. You will now see the total number of connections made, and the total active connections, along with a break-down of connections per-worker, leading the pipe is the active, trailing the pipe is the total number of connections. 54 | 55 | Workers 56 | connections total: 60 57 | connections active: 0 58 | 0: 15 seconds 0|4 59 | 1: 15 seconds 0|1 60 | 2: 15 seconds 0|25 61 | 3: 15 seconds 0|30 62 | 63 | ### Request Statistics 64 | 65 | Cluster supports reporting on requests as well, currently only tallying up the total number, however is capable of much more. The REPL `stats()` output below is the result of passing `.use(cluster.stats({ connections: true, requests: true }))`. 66 | 67 | 68 | Workers 69 | connections total: 60 70 | connections active: 0 71 | requests total: 24064 72 | 0: 15 seconds 0|4|3358 73 | 1: 15 seconds 0|1|1126 74 | 2: 15 seconds 0|25|9613 75 | 3: 15 seconds 0|30|9967 76 | 77 | ### Events 78 | 79 | When the options shown above are used, events are also emitted, so even if you do not plan on using the REPL, these events may be helpful to other plugins. 80 | 81 | - `client connection`, worker 82 | - `client disconnection`, worker 83 | - `client request`, worker, request 84 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | repl 2 | -------------------------------------------------------------------------------- /examples/app-cluster.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var cluster = require('../'); 6 | 7 | // $ telnet localhost 8888 8 | 9 | cluster('app.js') 10 | .set('workers', 4) 11 | .use(cluster.logger('logs')) 12 | .use(cluster.stats({ connections: true, requests: true })) 13 | .use(cluster.repl(8888, '127.0.0.1')) 14 | .use(cluster.debug()) 15 | .listen(3000); -------------------------------------------------------------------------------- /examples/app.js: -------------------------------------------------------------------------------- 1 | 2 | var http = require('http'); 3 | 4 | module.exports = http.createServer(function(req, res){ 5 | console.log('%s %s', req.method, req.url); 6 | var body = 'Hello'; 7 | res.writeHead(200, { 'Content-Length': body.length }); 8 | res.end(body); 9 | }); 10 | 11 | -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | var body = 'Hello World' 10 | , len = body.length; 11 | var server = http.createServer(function(req, res){ 12 | res.writeHead(200, { 'Content-Length': len }); 13 | res.end(body); 14 | }); 15 | 16 | cluster(server) 17 | .set('workers', 4) 18 | .set('working directory', '/') 19 | .listen(3000); -------------------------------------------------------------------------------- /examples/cli-app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var http = require('http'); 7 | 8 | module.exports = http.createServer(function(req, res){ 9 | console.log('%s %s', req.method, req.url); 10 | var body = 'Hello World'; 11 | res.writeHead(200, { 'Content-Length': body.length }); 12 | res.end(body); 13 | }); -------------------------------------------------------------------------------- /examples/cli.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../'); 7 | 8 | // Launch the cluster: 9 | // $ nohup node examples/cli.js & 10 | 11 | // Check the status: 12 | // $ node examples/cli.js status 13 | 14 | // View other commands: 15 | // $ node examples/cli.js --help 16 | 17 | cluster('cli-app') 18 | .use(cluster.pidfiles()) 19 | .use(cluster.debug()) 20 | .use(cluster.cli()) 21 | .listen(3000); 22 | -------------------------------------------------------------------------------- /examples/connect.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , connect = require('connect') 8 | , http = require('http'); 9 | 10 | // setup: 11 | // $ npm install connect 12 | 13 | var server = connect.createServer(); 14 | 15 | server.use(function(req, res, next){ 16 | var body = 'Hello World'; 17 | res.writeHead(200, { 'Content-Length': body.length }); 18 | res.end(body); 19 | }); 20 | 21 | cluster(server) 22 | .set('workers', 4) 23 | .use(cluster.debug()) 24 | .listen(3000); -------------------------------------------------------------------------------- /examples/debug.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | var server = http.createServer(function(req, res){ 10 | console.log('%s %s', req.method, req.url); 11 | var body = 'Hello World'; 12 | res.writeHead(200, { 'Content-Length': body.length }); 13 | res.end(body); 14 | }); 15 | 16 | cluster(server) 17 | .set('workers', 4) 18 | .use(cluster.logger('logs', 'debug')) 19 | .use(cluster.debug()) 20 | .listen(3000); -------------------------------------------------------------------------------- /examples/envs.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | var body = 'Hello World' 10 | , len = body.length; 11 | var server = http.createServer(function(req, res){ 12 | res.writeHead(200, { 'Content-Length': len }); 13 | res.end(body); 14 | }); 15 | 16 | // use: 17 | 18 | // cluster(server) 19 | // .set('working directory', '/') 20 | // .in('production').set('workers', 4) 21 | // .in('development').set('workers', 1) 22 | // .in('production').use(cluster.logger()) 23 | // .in('production').use(cluster.pidfiles()) 24 | // .in('development').use(cluster.logger('logs', 'debug')) 25 | // .in('development').use(cluster.debug()) 26 | // .in('production').listen(80) 27 | // .in('development').listen(3000); 28 | 29 | // or groups: 30 | 31 | cluster(server) 32 | .set('working directory', '/') 33 | .in('development') 34 | .set('workers', 1) 35 | .use(cluster.logger('logs', 'debug')) 36 | .use(cluster.debug()) 37 | .use(cluster.repl(8888)) 38 | .listen(3000) 39 | .in('production') 40 | .set('workers', 4) 41 | .use(cluster.logger()) 42 | .use(cluster.pidfiles()) 43 | .listen(80); 44 | -------------------------------------------------------------------------------- /examples/error.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | var server = http.createServer(function(req, res){ 10 | if (5 == (Math.random() * 10 | 0)) throw new Error('failed!'); 11 | console.log('%s %s', req.method, req.url); 12 | var body = 'Hello World'; 13 | res.writeHead(200, { 'Content-Length': body.length }); 14 | res.end(body); 15 | }); 16 | 17 | var proc = cluster(server) 18 | .use(cluster.debug()) 19 | .use(cluster.stats()) 20 | .use(cluster.repl(__dirname + '/repl')) 21 | .listen(3000); 22 | 23 | if (proc.isWorker) { 24 | // you can register your own exceptionHandler 25 | // which will prevent Cluster from add its own. This 26 | // means the workers will be harder to kill, however 27 | // if you do not employ additional logic, connections 28 | // will remain open until timeout. 29 | process.on('uncaughtException', function(err){ 30 | console.error(err); 31 | }); 32 | } -------------------------------------------------------------------------------- /examples/express.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , express = require('express') 8 | , http = require('http'); 9 | 10 | // setup: 11 | // $ npm install express 12 | 13 | var app = express.createServer(); 14 | 15 | app.get('/', function(req, res){ 16 | res.send('Hello World'); 17 | }); 18 | 19 | cluster(app) 20 | .set('workers', 4) 21 | .use(cluster.debug()) 22 | .listen(3000); -------------------------------------------------------------------------------- /examples/logger.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | var server = http.createServer(function(req, res){ 10 | console.log('%s %s', req.method, req.url); 11 | var body = 'Hello World'; 12 | res.writeHead(200, { 'Content-Length': body.length }); 13 | res.end(body); 14 | }); 15 | 16 | cluster(server) 17 | .use(cluster.logger()) 18 | .listen(3000); -------------------------------------------------------------------------------- /examples/logs/.gitignore: -------------------------------------------------------------------------------- 1 | *.log -------------------------------------------------------------------------------- /examples/pids.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | var body = 'Hello World' 10 | , len = body.length; 11 | var server = http.createServer(function(req, res){ 12 | res.writeHead(200, { 'Content-Length': len }); 13 | res.end(body); 14 | }); 15 | 16 | cluster(server) 17 | .use(cluster.debug()) 18 | .use(cluster.pidfiles()) 19 | .use(cluster.logger()) 20 | .listen(3000); -------------------------------------------------------------------------------- /examples/pids/.gitignore: -------------------------------------------------------------------------------- 1 | *.pid 2 | -------------------------------------------------------------------------------- /examples/reload.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | // try loading, and changing "Hello", to "Hello World" 10 | 11 | var body = 'Hello' 12 | , len = body.length; 13 | var server = http.createServer(function(req, res){ 14 | res.writeHead(200, { 'Content-Length': len }); 15 | res.end(body); 16 | }); 17 | 18 | cluster(server) 19 | // lower worker count will reload faster, 20 | // however more will work just fine 21 | .set('workers', 1) 22 | .use(cluster.reload()) 23 | .use(cluster.debug()) 24 | .listen(3000); -------------------------------------------------------------------------------- /examples/repl-unix.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | var server = http.createServer(function(req, res){ 10 | console.log('%s %s', req.method, req.url); 11 | var body = 'Hello World'; 12 | res.writeHead(200, { 'Content-Length': body.length }); 13 | res.end(body); 14 | }); 15 | 16 | // custom repl function 17 | 18 | cluster.repl.define('echo', function(master, sock, msg){ 19 | sock.write(msg + '\n'); 20 | }, 'echo the given message'); 21 | 22 | // $ telnet /path/to/examples/repl 23 | 24 | cluster(server) 25 | .set('workers', 1) 26 | .use(cluster.logger()) 27 | .use(cluster.repl('/tmp/repl')) 28 | .listen(3000); -------------------------------------------------------------------------------- /examples/repl.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | var server = http.createServer(function(req, res){ 10 | console.log('%s %s', req.method, req.url); 11 | var body = 'Hello'; 12 | res.writeHead(200, { 'Content-Length': body.length }); 13 | res.end(body); 14 | }); 15 | 16 | // custom repl function 17 | 18 | cluster.repl.define('echo', function(master, sock, msg){ 19 | sock.write(msg + '\n'); 20 | }, 'echo the given message'); 21 | 22 | // $ telnet localhost 8888 23 | cluster(server) 24 | .set('workers', 4) 25 | .use(cluster.logger('logs')) 26 | .use(cluster.stats({ connections: true, requests: true })) 27 | .use(cluster.repl(8889, '127.0.0.1')) 28 | .use(cluster.debug()) 29 | .listen(3002); 30 | -------------------------------------------------------------------------------- /examples/single.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | var body = 'Hello World' 10 | , len = body.length; 11 | var server = http.createServer(function(req, res){ 12 | res.writeHead(200, { 'Content-Length': len }); 13 | res.end(body); 14 | }); 15 | 16 | cluster(server) 17 | .set('working directory', '/') 18 | .set('workers', 1) 19 | .listen(3000); -------------------------------------------------------------------------------- /examples/standalone.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../'); 7 | 8 | var proc = cluster() 9 | .set('workers', 4) 10 | .use(cluster.debug()) 11 | .start(); 12 | 13 | if (proc.isWorker) { 14 | var id = process.env.CLUSTER_WORKER; 15 | console.log(' worker #%d started', id); 16 | setInterval(function(){ 17 | console.log(' processing job from worker #%d', id); 18 | }, 3000); 19 | } -------------------------------------------------------------------------------- /examples/tcp.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , net = require('net'); 8 | 9 | var server = net.createServer(function(sock){ 10 | sock.write('echo server of amazingness\n'); 11 | sock.pipe(sock); 12 | }); 13 | 14 | cluster(server) 15 | .set('workers', 4) 16 | .use(cluster.debug()) 17 | .use(cluster.stats()) 18 | .use(cluster.repl(8888)) 19 | .listen(3000); 20 | -------------------------------------------------------------------------------- /examples/unix.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | var body = 'Hello World' 10 | , len = body.length; 11 | var server = http.createServer(function(req, res){ 12 | res.writeHead(200, { 'Content-Length': len }); 13 | res.end(body); 14 | }); 15 | 16 | cluster(server) 17 | .set('working directory', '/') 18 | .listen('/tmp/server.sock'); -------------------------------------------------------------------------------- /examples/vhost.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , connect = require('connect'); 8 | 9 | // setup: 10 | // $ npm install connect 11 | // $ edit /etc/hosts 12 | 13 | var server = connect(); 14 | 15 | var foo = connect().use(function(req, res){ 16 | var body = 'Hello from foo.com'; 17 | res.writeHead(200, { 'Content-Length': body.length }); 18 | res.end(body); 19 | }); 20 | 21 | var bar = connect().use(function(req, res){ 22 | var body = 'Hello from bar.com'; 23 | res.writeHead(200, { 'Content-Length': body.length }); 24 | res.end(body); 25 | }); 26 | 27 | server.use(connect.vhost('foo.com', foo)); 28 | server.use(connect.vhost('bar.com', bar)); 29 | 30 | cluster(server) 31 | .set('workers', 4) 32 | .use(cluster.debug()) 33 | .listen(3000); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/cluster'); -------------------------------------------------------------------------------- /lib/cluster.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Cluster 4 | * Copyright(c) 2011 LearnBoost 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var Master = require('./master') 13 | , fs = require('fs'); 14 | 15 | /** 16 | * Export `start` as the module. 17 | */ 18 | 19 | exports = module.exports = start; 20 | 21 | /** 22 | * Library version. 23 | */ 24 | 25 | exports.version = '0.7.7'; 26 | 27 | /** 28 | * Expose utils. 29 | */ 30 | 31 | exports.utils = require('./utils'); 32 | 33 | /** 34 | * Start a new `Master` with the given `server`. 35 | * 36 | * @param {http.Server} server 37 | * @return {Master} 38 | * @api public 39 | */ 40 | 41 | function start(server) { 42 | return new Master(server); 43 | } 44 | 45 | /** 46 | * Expose middleware via lazy-requires. 47 | */ 48 | 49 | fs.readdirSync(__dirname + '/plugins').forEach(function(plugin){ 50 | plugin = plugin.replace('.js', ''); 51 | exports.__defineGetter__(plugin, function(){ 52 | return require('./plugins/' + plugin); 53 | }); 54 | }); -------------------------------------------------------------------------------- /lib/master.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Cluster - Master 4 | * Copyright(c) 2011 LearnBoost 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var Worker = require('./worker') 13 | , EventEmitter = require('events').EventEmitter 14 | , dirname = require('path').dirname 15 | , spawn = require('child_process').spawn 16 | , utils = require('./utils') 17 | , fsBinding = process.binding('fs') 18 | , netBinding = process.binding('net') 19 | , bind = netBinding.bind 20 | , listen = netBinding.listen 21 | , socket = netBinding.socket 22 | , socketpair = netBinding.socketpair 23 | , close = netBinding.close 24 | , unlink = fsBinding.unlink 25 | , dgram = require('dgram') 26 | , tty = require('tty') 27 | , net = require('net') 28 | , fs = require('fs') 29 | , os = require('os'); 30 | 31 | /** 32 | * Node binary. 33 | */ 34 | 35 | var node = process.execPath; 36 | 37 | /** 38 | * Start a new `Master` with the given `server` or filename to 39 | * a node module exporting a server. 40 | * 41 | * Options: 42 | * 43 | * - `workers` Number of workers to spawn, defaults to the number of CPUs 44 | * - 'working directory` Working directory defaulting to the script's dir 45 | * - 'backlog` Connection backlog, defaulting to 128 46 | * - 'socket port` Master socket port defaulting to `8989` 47 | * - 'timeout` Worker shutdown timeout in milliseconds, defaulting to 60,000 48 | * - 'user` User id / name 49 | * - 'group` Group id / name 50 | * - `title` Master process title, defaults to "cluster master" 51 | * - `worker title` Worker process title, defaults to "cluster worker {n}" 52 | * 53 | * Events: 54 | * 55 | * - `start`. When the IPC server is prepped 56 | * - `worker`. When a worker is spawned, passing the `worker` 57 | * - `listening`. When the server is listening for connections 58 | * - `closing`. When master is shutting down 59 | * - `close`. When master has completed shutting down 60 | * - `worker killed`. When a worker has died 61 | * - `worker exception`. Worker uncaughtException. Receives the worker / exception 62 | * - `worker removed`. Worker removed via `spawn(-n)` 63 | * - `kill`. When a `signal` is being sent to all workers 64 | * - `restarting`. Restart requested by REPL or signal. Receives an object 65 | * which can be patched in order to preserve plugin state. 66 | * - `restart`. Restart complete, new master established, previous died. 67 | * Receives an object with state preserved by the `restarting` event. 68 | * 69 | * Signals: 70 | * 71 | * - `SIGINT` hard shutdown 72 | * - `SIGTERM` hard shutdown 73 | * - `SIGQUIT` graceful shutdown 74 | * - `SIGUSR2` graceful restart 75 | * 76 | * @param {net.Server|String} server 77 | * @return {Master} 78 | * @api public 79 | */ 80 | 81 | var Master = module.exports = function Master(server) { 82 | var self = this; 83 | this.server = server; 84 | this.plugins = []; 85 | this.children = []; 86 | this.state = 'active'; 87 | this.startup = new Date; 88 | this._killed = 0; 89 | 90 | // grab server root 91 | this.cmd = process.argv.slice(1); 92 | this.dir = dirname(this.cmd[0]); 93 | 94 | // environment 95 | this.env = process.env.NODE_ENV || 'development'; 96 | 97 | // defaults 98 | this.options = { 99 | 'backlog': 128 100 | , 'working directory': this.dir 101 | , 'socket port': 8989 102 | , 'socket addr': '127.0.0.1' 103 | , 'timeout': 60000 104 | , 'restart threshold': 'development' == this.env ? 5000 : 60000 105 | , 'restart timeout': 'development' == this.env ? 5000 : 60000 106 | , 'title': 'cluster' 107 | , 'worker title': 'cluster worker' 108 | }; 109 | 110 | // parent master pid 111 | this.ppid = process.env.CLUSTER_PARENT_PID 112 | ? parseInt(process.env.CLUSTER_PARENT_PID, 10) 113 | : null; 114 | 115 | // process is a worker 116 | this.isWorker = !! process.env.CLUSTER_MASTER_PID; 117 | 118 | // process is a child (worker or master replacement) 119 | this.isChild = this.isWorker || !! process.env.CLUSTER_REPLACEMENT_MASTER; 120 | 121 | // process is master 122 | this.isMaster = ! this.isWorker; 123 | 124 | // process id 125 | this.pid = process.pid; 126 | if (this.isMaster) process.env.CLUSTER_MASTER_PID = this.pid; 127 | 128 | // custom worker fds, defaults to std{out,err} 129 | this.customFds = [1, 2]; 130 | 131 | // resolve server filename 132 | if (this.isWorker && 'string' == typeof this.server) { 133 | this.server = require(this.resolve(this.server)); 134 | } 135 | 136 | // IPC is prepped 137 | this.on('start', function(){ 138 | process.chdir(self.options['working directory']); 139 | }); 140 | 141 | // spawn our workers 142 | this.on('listening', function(){ 143 | self.spawn(self.options.workers); 144 | self.listening = true; 145 | }); 146 | 147 | // kill children on master exception 148 | if (this.isMaster) { 149 | process.on('uncaughtException', function(err){ 150 | self.kill('SIGKILL'); 151 | console.error(err.stack || String(err)); 152 | process.exit(1); 153 | }); 154 | } 155 | }; 156 | 157 | /** 158 | * Interit from `EventEmitter.prototype`. 159 | */ 160 | 161 | Master.prototype.__proto__ = EventEmitter.prototype; 162 | 163 | /** 164 | * Worker is a receiver. 165 | */ 166 | 167 | require('./mixins/receiver')(Master.prototype); 168 | 169 | /** 170 | * Resolve `path` relative to the server file being executed. 171 | * 172 | * @param {String} path 173 | * @return {String} 174 | * @api public 175 | */ 176 | 177 | Master.prototype.resolve = function(path){ 178 | return '/' == path[0] 179 | ? path 180 | : this.dir + '/' + path; 181 | }; 182 | 183 | /** 184 | * Return `true` when the environment set by `Master#in()` 185 | * matches __NODE_ENV__. 186 | * 187 | * @return {Boolean} 188 | * @api private 189 | */ 190 | 191 | Master.prototype.__defineGetter__('environmentMatches', function(){ 192 | if (this._env) 193 | return this.env == this._env || 'all' == this._env; 194 | return true; 195 | }); 196 | 197 | /** 198 | * Invoke masters's `method` with worker `id`. (called from Worker) 199 | * 200 | * @param {Number} id 201 | * @param {String} method 202 | * @param {...} args 203 | * @api private 204 | */ 205 | 206 | Master.prototype.call = function(id, method){ 207 | this.sock = this.sock || dgram.createSocket('udp4'); 208 | 209 | var msg = new Buffer(utils.frame({ 210 | args: utils.toArray(arguments, 2) 211 | , method: method 212 | , id: id 213 | })); 214 | 215 | this.sock.send( 216 | msg 217 | , 0 218 | , msg.length 219 | , this.options['socket port'] 220 | , this.options['socket addr']); 221 | }; 222 | 223 | /** 224 | * Perform setup tasks then invoke `fn()` when present. 225 | * 226 | * @param {Function} fn 227 | * @return {Master} for chaining 228 | * @api public 229 | */ 230 | 231 | Master.prototype.start = function(fn){ 232 | var self = this; 233 | 234 | // deferred title 235 | process.title = this.options.title; 236 | 237 | // prevent listen 238 | if (this.preventDefault) return this; 239 | 240 | // env match 241 | if (this.environmentMatches) { 242 | // worker process 243 | if (this.isWorker) { 244 | this.worker = new Worker(this); 245 | this.worker.start(); 246 | // master process 247 | } else if (fn) { 248 | fn(); 249 | // standalone 250 | } else { 251 | this.on('start', function(){ self.emit('listening'); }); 252 | if (this.isChild) this.acceptFd(); 253 | this.setupIPC(); 254 | } 255 | } 256 | 257 | return this; 258 | }; 259 | 260 | /** 261 | * Defer `http.Server#listen()` call. 262 | * 263 | * @param {Number|String} port or unix domain socket path 264 | * @param {String|Function} host or callback 265 | * @param {Function} callback 266 | * @return {Master} for chaining 267 | * @api public 268 | */ 269 | 270 | Master.prototype.listen = function(port, host, callback){ 271 | var self = this; 272 | if (!this.environmentMatches) return this; 273 | if ('function' == typeof host) callback = host, host = null; 274 | this.port = port; 275 | this.host = host; 276 | this.callback = callback; 277 | return this.start(function(){ 278 | self.on('start', function(){ 279 | self.startListening(!self.isChild); 280 | }); 281 | 282 | if (self.isChild) { 283 | self.acceptFd(); 284 | } else { 285 | self.createSocket(function(err, fd){ 286 | if (err) throw err; 287 | self.fd = fd; 288 | self.setupIPC(); 289 | }); 290 | } 291 | }); 292 | }; 293 | 294 | /** 295 | * Create / return IPC socket. 296 | * 297 | * @api private 298 | */ 299 | 300 | Master.prototype.IPCSocket = function(){ 301 | var self = this; 302 | if (this._sock) return this._sock; 303 | this._sock = dgram.createSocket('udp4'); 304 | this._sock.on('message', function(msg, info){ 305 | try { 306 | msg = JSON.parse(msg.toString('ascii')); 307 | self.invoke(msg.method, msg.args, self.children[msg.id]); 308 | } catch (err) { 309 | console.error(err.stack || String(err)); 310 | } 311 | }); 312 | return this._sock; 313 | }; 314 | 315 | /** 316 | * Setup IPC. 317 | * 318 | * @api private 319 | */ 320 | 321 | Master.prototype.setupIPC = function(){ 322 | var self = this; 323 | 324 | // signal handlers 325 | this.registerSignalHandlers(); 326 | 327 | // Default worker to the # of cpus 328 | this.defaultWorkers(); 329 | 330 | // udp server for IPC 331 | this.IPCSocket().on('listening', function(){ 332 | process.nextTick(function(){ 333 | self.emit('start'); 334 | }); 335 | }); 336 | 337 | // bind 338 | this.IPCSocket().bind( 339 | this.options['socket port'] 340 | , this.options['socket addr']); 341 | }; 342 | 343 | /** 344 | * Conditionally perform the following action, if 345 | * __NODE_ENV__ matches `env`. 346 | * 347 | * Examples: 348 | * 349 | * cluster(server) 350 | * .in('development').use(cluster.debug()) 351 | * .in('development').listen(3000) 352 | * .in('production').listen(80); 353 | * 354 | * @param {String} env 355 | * @return {Master} self or stubs 356 | * @api public 357 | */ 358 | 359 | Master.prototype.in = function(env){ 360 | this._env = env; 361 | return this; 362 | }; 363 | 364 | /** 365 | * Set option `key` to `val`. 366 | * 367 | * @param {String} key 368 | * @param {Mixed} val 369 | * @return {Master} for chaining 370 | * @api public 371 | */ 372 | 373 | Master.prototype.set = function(key, val){ 374 | if (this.environmentMatches) this.options[key] = val; 375 | return this; 376 | }; 377 | 378 | /** 379 | * Invoke `fn(master)`. 380 | * 381 | * @param {Function} fn 382 | * @api public 383 | */ 384 | 385 | Master.prototype.do = function(fn){ 386 | if (this.environmentMatches) fn.call(this, this); 387 | return this; 388 | }; 389 | 390 | /** 391 | * Check if `option` has been set. 392 | * 393 | * @param {String} option 394 | * @return {Boolean} 395 | * @api public 396 | */ 397 | 398 | Master.prototype.has = function(option){ 399 | return !! this.options[option]; 400 | }; 401 | 402 | /** 403 | * Use the given `plugin`. 404 | * 405 | * @param {Function} plugin 406 | * @return {Master} for chaining 407 | * @api public 408 | */ 409 | 410 | Master.prototype.use = function(plugin){ 411 | if (this.environmentMatches) { 412 | this.plugins.push(plugin); 413 | if (this.isWorker) { 414 | plugin.enableInWorker && plugin(this); 415 | } else { 416 | plugin(this); 417 | } 418 | } 419 | return this; 420 | }; 421 | 422 | /** 423 | * Create listening socket and callback `fn(err, fd)`. 424 | * 425 | * @return {Function} fn 426 | * @api private 427 | */ 428 | 429 | Master.prototype.createSocket = function(fn){ 430 | var self = this 431 | , ipv; 432 | 433 | // explicit host 434 | if (this.host) { 435 | // ip 436 | if (ipv = net.isIP(this.host)) { 437 | fn(null, socket('tcp' + ipv)); 438 | // lookup 439 | } else { 440 | require('dns').lookup(this.host, function(err, ip, ipv){ 441 | if (err) return fn(err); 442 | self.host = ip; 443 | fn(null, socket('tcp' + ipv)); 444 | }); 445 | } 446 | // local socket 447 | } else if ('string' == typeof this.port) { 448 | fn(null, socket('unix')); 449 | // only port 450 | } else if ('number' == typeof this.port) { 451 | fn(null, socket('tcp4')); 452 | } 453 | }; 454 | 455 | /** 456 | * Register signal handlers. 457 | * 458 | * @api private 459 | */ 460 | 461 | Master.prototype.registerSignalHandlers = function(){ 462 | var self = this; 463 | process.on('SIGINT', this.destroy.bind(this)); 464 | process.on('SIGTERM', this.destroy.bind(this)); 465 | process.on('SIGQUIT', this.close.bind(this)); 466 | process.on('SIGUSR2', this.attemptRestart.bind(this)); 467 | process.on('SIGCHLD', this.maintainWorkerCount.bind(this)); 468 | }; 469 | 470 | /** 471 | * Default workers to the number of cpus available. 472 | * 473 | * @api private 474 | */ 475 | 476 | Master.prototype.defaultWorkers = function(){ 477 | if (!this.has('workers')) { 478 | this.set('workers', os 479 | ? os.cpus().length 480 | : 1); 481 | } 482 | }; 483 | 484 | /** 485 | * Restart workers only, sending `signal` defaulting 486 | * to __SIGQUIT__. 487 | * 488 | * @param {Type} name 489 | * @return {Type} 490 | * @api public 491 | */ 492 | 493 | Master.prototype.restartWorkers = function(signal){ 494 | this.kill(signal || 'SIGQUIT'); 495 | }; 496 | 497 | /** 498 | * Maintain worker count, re-spawning if necessary. 499 | * 500 | * @api private 501 | */ 502 | 503 | Master.prototype.maintainWorkerCount = function(){ 504 | this.children.forEach(function(worker){ 505 | var pid = worker.proc.pid; 506 | if (!pid) this.workerKilled(worker); 507 | }, this); 508 | }; 509 | 510 | /** 511 | * Remove `n` workers with `signal` 512 | * defaulting to __SIGQUIT__. 513 | * 514 | * @param {Number} n 515 | * @param {String} signal 516 | * @api public 517 | */ 518 | 519 | Master.prototype.remove = function(n, signal){ 520 | if (!arguments.length) n = 1; 521 | var len = this.children.length 522 | , worker; 523 | 524 | // cap at worker len 525 | if (n > len) n = len; 526 | 527 | // remove the workers 528 | while (n--) { 529 | worker = this.children.pop(); 530 | worker.proc.kill(signal || 'SIGQUIT'); 531 | this.emit('worker removed', worker); 532 | this.removeWorker(worker.id); 533 | } 534 | }; 535 | 536 | /** 537 | * Remove worker `id`. 538 | * 539 | * @param {Number} id 540 | * @api public 541 | */ 542 | 543 | Master.prototype.removeWorker = function(id){ 544 | var worker = this.children[id]; 545 | if (!worker) return; 546 | if (worker.fds) { 547 | close(worker.fds[0]); 548 | close(worker.fds[1]); 549 | } 550 | delete this.children[id]; 551 | }; 552 | 553 | /** 554 | * Spawn `n` workers. 555 | * 556 | * @param {Number} n 557 | * @api public 558 | */ 559 | 560 | Master.prototype.spawn = function(n){ 561 | if (!arguments.length) n = 1; 562 | while (n--) this.spawnWorker(); 563 | }; 564 | 565 | /** 566 | * Spawn a worker with optional `id`. 567 | * 568 | * @param {Number} id 569 | * @return {Worker} 570 | * @api private 571 | */ 572 | 573 | Master.prototype.spawnWorker = function(id){ 574 | var worker; 575 | 576 | // id given 577 | if ('number' == typeof id) { 578 | worker = new Worker(this).spawn(id) 579 | this.children[id] = worker; 580 | worker.id = id; 581 | // generate an id 582 | } else { 583 | worker = new Worker(this).spawn(this.children.length); 584 | this.children.push(worker); 585 | } 586 | 587 | var obj = { 588 | method: 'connect' 589 | , args: [worker.id, this.options] 590 | }; 591 | 592 | worker.sock.write(utils.frame(obj), 'ascii', this.fd); 593 | 594 | // emit 595 | this.emit('worker', worker); 596 | 597 | return worker; 598 | }; 599 | 600 | /** 601 | * Graceful shutdown, wait for all workers 602 | * to reply before exiting. 603 | * 604 | * @api public 605 | */ 606 | 607 | Master.prototype.close = function(){ 608 | this.state = 'graceful shutdown'; 609 | this.emit('closing'); 610 | this.kill('SIGQUIT'); 611 | this.pendingDeaths = this.children.length; 612 | }; 613 | 614 | /** 615 | * Hard shutdwn, immediately kill all workers. 616 | * 617 | * @api public 618 | */ 619 | 620 | Master.prototype.destroy = function(){ 621 | this.state = 'hard shutdown'; 622 | this.emit('closing'); 623 | this.kill('SIGKILL'); 624 | this._destroy(); 625 | }; 626 | 627 | /** 628 | * Attempt restart, while respecting the `restart threshold` 629 | * setting, to help prevent recursive restarts. 630 | * 631 | * @param {String} sig 632 | * @api private 633 | */ 634 | 635 | Master.prototype.attemptRestart = function(sig){ 636 | var uptime = new Date - this.startup 637 | , threshold = this.options['restart threshold'] 638 | , timeout = this.options['restart timeout']; 639 | 640 | if (this.__restarting) return; 641 | 642 | if (uptime < threshold) { 643 | this.__restarting = true; 644 | this.emit('cyclic restart'); 645 | setTimeout(function(self){ 646 | self.restart(sig); 647 | }, timeout, this); 648 | } else { 649 | this.restart(sig); 650 | } 651 | }; 652 | 653 | /** 654 | * Restart all workers, by sending __SIGQUIT__ 655 | * or `sig` to them, enabling master to re-spawn. 656 | * 657 | * @param {String} sig 658 | * @return {ChildProcess} replacement master process 659 | * @api public 660 | */ 661 | 662 | Master.prototype.restart = function(sig){ 663 | var data = {} 664 | , proc = this.spawnMaster(); 665 | 666 | // pass object to plugins, allowing them 667 | // to patch it, and utilize the data in 668 | // the new Master 669 | this.emit('restarting', data); 670 | proc.sock.write(utils.frame({ 671 | method: 'connectMaster' 672 | , args: [sig || 'SIGQUIT'] 673 | }), 'ascii', this.fd); 674 | 675 | this.on('close', function(){ 676 | proc.sock.write(utils.frame({ 677 | method: 'masterKilled' 678 | , args: [data] 679 | }), 'ascii'); 680 | }); 681 | 682 | return proc; 683 | }; 684 | 685 | /** 686 | * Spawn a new master process. 687 | * 688 | * @return {ChildProcess} 689 | * @api private 690 | */ 691 | 692 | Master.prototype.spawnMaster = function(){ 693 | var fds = socketpair() 694 | , customFds = [fds[0], 1, 2] 695 | , env = {}; 696 | 697 | // merge current env 698 | for (var key in process.env) { 699 | env[key] = process.env[key]; 700 | } 701 | 702 | delete env.CLUSTER_MASTER_PID; 703 | env.CLUSTER_REPLACEMENT_MASTER = 1; 704 | env.CLUSTER_PARENT_PID = this.pid; 705 | 706 | // spawn new master process 707 | var proc = spawn(node, this.cmd, { 708 | customFds: customFds 709 | , env: env 710 | }); 711 | 712 | // unix domain socket for ICP + fd passing 713 | proc.sock = new net.Socket(fds[1], 'unix'); 714 | return proc; 715 | }; 716 | 717 | /** 718 | * Master replacement connected. 719 | * 720 | * @param {String} sig 721 | * @api private 722 | */ 723 | 724 | Master.prototype.connectMaster = function(sig){ 725 | var self = this; 726 | 727 | function kill(){ 728 | process.kill(self.ppid, sig); 729 | } 730 | 731 | if (this.listening) return kill(); 732 | this.on('listening', kill); 733 | }; 734 | 735 | /** 736 | * Original master has died aka 'retired', 737 | * we now fire the 'restart' event. 738 | * 739 | * @param {Object} data 740 | * @api private 741 | */ 742 | 743 | Master.prototype.masterKilled = function(data){ 744 | this.emit('restart', data); 745 | }; 746 | 747 | /** 748 | * Accept fd from parent master, then `setupIPC()`. 749 | * 750 | * @api private 751 | */ 752 | 753 | Master.prototype.acceptFd = function(){ 754 | var self = this 755 | , stdin = new net.Socket(0, 'unix'); 756 | 757 | // set fd and start master 758 | stdin.setEncoding('ascii'); 759 | stdin.on('fd', function(fd){ 760 | self.fd = fd; 761 | self.setupIPC(); 762 | }); 763 | 764 | // frame commands from the parent master 765 | stdin.on('data', this.frame.bind(this)); 766 | stdin.resume(); 767 | }; 768 | 769 | /** 770 | * Close servers and emit 'close' before exiting. 771 | * 772 | * @api private 773 | */ 774 | 775 | Master.prototype._destroy = function(){ 776 | this.IPCSocket().close(); 777 | if (this.fd) close(this.fd); 778 | this.emit('close'); 779 | process.nextTick(process.exit.bind(process)); 780 | }; 781 | 782 | /** 783 | * Worker is connected. 784 | * 785 | * @param {Worker} worker 786 | * @api private 787 | */ 788 | 789 | Master.prototype.connect = function(worker){ 790 | this.emit('worker connected', worker); 791 | }; 792 | 793 | /** 794 | * Start listening, when `shouldBind` is `true` the socket 795 | * will be bound, and will start listening for connections. 796 | * 797 | * @param {Boolean} shouldBind 798 | * @api private 799 | */ 800 | 801 | Master.prototype.startListening = function(shouldBind){ 802 | var self = this; 803 | 804 | // remove unix domain socket 805 | if ('string' == typeof this.port && shouldBind) { 806 | fs.unlink(this.port, function(err){ 807 | if (err && 'ENOENT' != err.code) throw err; 808 | startListening(); 809 | }); 810 | } else { 811 | startListening(); 812 | } 813 | 814 | // bind / listen 815 | function startListening() { 816 | if (shouldBind) { 817 | try { 818 | bind(self.fd, self.port, self.host); 819 | listen(self.fd, self.options.backlog); 820 | } catch(e) { 821 | self.kill('SIGKILL'); 822 | throw e; 823 | } 824 | } 825 | self.callback && self.callback(); 826 | self.emit('listening'); 827 | } 828 | }; 829 | 830 | /** 831 | * The given `worker` has been killed. 832 | * Emit the "worker killed" event, remove 833 | * the worker, and re-spawn depending on 834 | * the master state. 835 | * 836 | * @api private 837 | */ 838 | 839 | Master.prototype.workerKilled = function(worker){ 840 | // if we have many failing workers at boot 841 | // then we likely have a serious issue. 842 | if (new Date - this.startup < 20000) { 843 | if (++this._killed == 20) { 844 | console.error(''); 845 | console.error('Cluster detected over 20 worker deaths in the first'); 846 | console.error('20 seconds of life, there is most likely'); 847 | console.error('a serious issue with your server.'); 848 | console.error(''); 849 | console.error('aborting.'); 850 | console.error(''); 851 | process.exit(1); 852 | } 853 | } 854 | 855 | // emit event 856 | this.emit('worker killed', worker); 857 | 858 | // always remove worker 859 | this.removeWorker(worker.id); 860 | 861 | // state specifics 862 | switch (this.state) { 863 | case 'hard shutdown': 864 | break; 865 | case 'graceful shutdown': 866 | --this.pendingDeaths || this._destroy(); 867 | break; 868 | default: 869 | this.spawnWorker(worker.id); 870 | } 871 | }; 872 | 873 | /** 874 | * `worker` received exception `err`. 875 | * 876 | * @api private 877 | */ 878 | 879 | Master.prototype.workerException = function(worker, err){ 880 | this.emit('worker exception', worker, err); 881 | }; 882 | 883 | /** 884 | * Received worker timeout. 885 | * 886 | * @api private 887 | */ 888 | 889 | Master.prototype.workerTimeout = function(worker, timeout){ 890 | this.emit('worker timeout', worker, timeout); 891 | }; 892 | 893 | /** 894 | * Worker waiting on `connections` to close. 895 | * 896 | * @api private 897 | */ 898 | 899 | Master.prototype.workerWaiting = function(worker, connections){ 900 | this.emit('worker waiting', worker, connections); 901 | }; 902 | 903 | /** 904 | * Send `sig` to all worker processes, defaults to __SIGTERM__. 905 | * 906 | * @param {String} sig 907 | * @api public 908 | */ 909 | 910 | Master.prototype.kill = function(sig){ 911 | var self = this; 912 | this.emit('kill', sig); 913 | this.children.forEach(function(worker){ 914 | worker.proc.kill(sig); 915 | }); 916 | }; 917 | -------------------------------------------------------------------------------- /lib/mixins/receiver.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Cluster - receiver mixin 4 | * Copyright(c) 2011 LearnBoost 5 | * MIT Licensed 6 | */ 7 | 8 | module.exports = function(obj){ 9 | 10 | /** 11 | * Initialize buffer. 12 | */ 13 | 14 | obj._buf = ''; 15 | 16 | /** 17 | * Frame incoming command, buffering the given `chunk` 18 | * until a frame is complete. Frames are delimited by a 19 | * line feed. 20 | * 21 | * @param {String} chunk 22 | * @api private 23 | */ 24 | 25 | obj.frame = function(chunk){ 26 | for (var i = 0, len = chunk.length; i < len; ++i) { 27 | if ('\n' == chunk[i]) { 28 | var worker 29 | , obj = JSON.parse(this._buf); 30 | this._buf = ''; 31 | if ('number' == typeof obj.id) worker = this.children[obj.id]; 32 | this.invoke(obj.method, obj.args, worker); 33 | } else { 34 | this._buf += chunk[i]; 35 | } 36 | } 37 | }; 38 | 39 | /** 40 | * Invoke `method` with the given `args`. 41 | * 42 | * @param {String} method 43 | * @param {Mixed} args 44 | * @param {Worker} worker 45 | * @api private 46 | */ 47 | 48 | obj.invoke = function(method, args, worker){ 49 | if (!method) return; 50 | if (!Array.isArray(args)) args = [args]; 51 | if (worker) args.unshift(worker); 52 | if (!this[method]) throw new Error('method ' + method + '() does not exist'); 53 | this[method].apply(this, args); 54 | }; 55 | }; -------------------------------------------------------------------------------- /lib/plugins/cli.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Cluster - cli 4 | * Copyright (c) 2011 LearnBoost 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var fs = require('fs') 13 | , Log = require('log'); 14 | 15 | /** 16 | * Commands. 17 | */ 18 | 19 | var commands = []; 20 | 21 | /** 22 | * Adds a command-line interface to your cluster. 23 | * 24 | * This plugin requires that you use `pidfiles()` 25 | * above `cli()`, so that the pidfile directory 26 | * is exposed. 27 | * 28 | * Examples: 29 | * 30 | * cluster(server) 31 | * .use(cluster.pidfiles()) 32 | * .use(cluster.cli()) 33 | * .listen(3000); 34 | * 35 | * Once set up our server script serves as both 36 | * the master, and the CLI. For example we may 37 | * still launch the server(s) as shown below. 38 | * 39 | * $ nohup node server.js & 40 | * 41 | * However now we may also utilize commands 42 | * provided by this plugin. 43 | * 44 | * $ node server.js status 45 | * 46 | * master 3281 dead 47 | * worker 0 3282 dead 48 | * 49 | * For more command information use `--help`. 50 | * 51 | * $ node server.js --help 52 | * 53 | * @return {Function} 54 | * @api public 55 | */ 56 | 57 | exports = module.exports = function(){ 58 | return function(master){ 59 | requirePIDs(master); 60 | 61 | // augment master 62 | master.killall = function(sig){ 63 | var pid = master.pidof('master'); 64 | try { 65 | // signal master 66 | process.kill(pid, sig); 67 | } catch (err) { 68 | if ('ESRCH' != err.code) throw err; 69 | // signal children individually 70 | master.workerpids().forEach(function(pid){ 71 | try { 72 | process.kill(pid, sig); 73 | } catch (err) { 74 | if ('ESRCH' != err.code) throw err; 75 | } 76 | }); 77 | } 78 | }; 79 | 80 | var args = process.argv.slice(2) 81 | , len = commands.length 82 | , command 83 | , arg; 84 | 85 | // parse arguments 86 | while (args.length) { 87 | arg = args.shift(); 88 | for (var i = 0; i < len; ++i) { 89 | command = commands[i]; 90 | if (~command.flags.indexOf(arg)) { 91 | command.callback(master); 92 | master.preventDefault = true; 93 | } 94 | } 95 | } 96 | } 97 | }; 98 | 99 | /** 100 | * Define command `name` with the given callback `fn(master)` 101 | * and a short `desc`. 102 | * 103 | * @param {String} name 104 | * @param {Function} fn 105 | * @param {String} desc 106 | * @return {Object} exports for chaining 107 | * @api public 108 | */ 109 | 110 | var define = exports.define = function(name, fn, desc){ 111 | commands.push({ 112 | flags: name.split(/ *, */) 113 | , desc: desc 114 | , callback: fn 115 | }); 116 | return exports; 117 | }; 118 | 119 | /** 120 | * Report master / worker status based on 121 | * the PID files saved by the pidfiles() 122 | * plugin. 123 | */ 124 | 125 | define('-s, --status, status', function(master){ 126 | var dir = master.pidfiles 127 | , files = fs.readdirSync(dir); 128 | 129 | // null signal failed previous 130 | // to this release 131 | if (process.version < 'v0.4.1') { 132 | console.log('status will not work with node < 0.4.1'); 133 | console.log('due to SIGTERM globbering the null signal'); 134 | process.exit(1); 135 | } 136 | 137 | console.log(); 138 | 139 | // only pids 140 | files.filter(function(file){ 141 | return file.match(/\.pid$/); 142 | // check status 143 | }).forEach(function(file){ 144 | var name = file.replace('.pid', '') 145 | , pid = master.pidof(name) 146 | , name = name.replace('.', ' ') 147 | , color 148 | , status; 149 | 150 | try { 151 | process.kill(pid, 0); 152 | status = 'alive'; 153 | color = '36'; 154 | } catch (err) { 155 | if ('ESRCH' == err.code) { 156 | color = '31'; 157 | status = 'dead'; 158 | } else { 159 | throw err; 160 | } 161 | } 162 | 163 | console.log(' %s\033[90m %d\033[0m \033[' + color + 'm%s\033[0m' 164 | , name 165 | , pid 166 | , status); 167 | }); 168 | 169 | console.log(); 170 | }, 'Output cluster status'); 171 | 172 | /** 173 | * Restart workers. 174 | */ 175 | 176 | define('-r, --restart, restart', function(master){ 177 | master.killall('SIGUSR2'); 178 | }, 'Restart master by sending the SIGUSR2 signal'); 179 | 180 | /** 181 | * Graceful shutdown. 182 | */ 183 | 184 | define('-g, --shutdown, shutdown', function(master){ 185 | master.killall('SIGQUIT'); 186 | }, 'Graceful shutdown by sending the SIGQUIT signal'); 187 | 188 | /** 189 | * Hard shutdown. 190 | */ 191 | 192 | define('-S, --stop, stop', function(master){ 193 | master.killall('SIGTERM'); 194 | }, 'Hard shutdown by sending the SIGTERM signal'); 195 | 196 | /** 197 | * Display help information. 198 | */ 199 | 200 | define('-h, --help, help', function(master){ 201 | console.log('\n Usage: node \n'); 202 | commands.forEach(function(command){ 203 | console.log(' ' 204 | + command.flags.join(', ') 205 | + '\n ' 206 | + '\033[90m' + command.desc + '\033[0m' 207 | + '\n'); 208 | }); 209 | console.log(); 210 | }, 'Show help information'); 211 | 212 | /** 213 | * Output cluster version. 214 | */ 215 | 216 | define('-v, --version', function(master){ 217 | console.log(require('../cluster').version); 218 | }, 'Output cluster version'); 219 | 220 | /** 221 | * Require `pidfiles()` plugin. 222 | * 223 | * @param {Master} master 224 | * @api private 225 | */ 226 | 227 | function requirePIDs(master) { 228 | if (master.pidfiles) return; 229 | throw new Error('cli() plugin requires pidfiles(), please add pidfiles() before cli()'); 230 | } -------------------------------------------------------------------------------- /lib/plugins/debug.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Cluster - debug 4 | * Copyright (c) 2011 LearnBoost 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Enable verbose debugging output. 10 | * 11 | * @return {Function} 12 | * @api public 13 | */ 14 | 15 | module.exports = function(options){ 16 | options = options || {}; 17 | 18 | // strip colors 19 | 20 | function color(text) { 21 | if (options.colors === false) return text.replace(/\033\[\d+m/g, ''); 22 | return text 23 | } 24 | 25 | // logger 26 | 27 | var log = { 28 | debug: function(str){ 29 | console.error(color(' \033[90mdebug - %s\033[0m'), str); 30 | }, 31 | info: function(str){ 32 | console.error(color(' info \033[90m- %s\033[0m'), str); 33 | }, 34 | warning: function(str){ 35 | console.error(color(' \033[33mwarning\033[0m \033[90m- %s\033[0m'), str); 36 | }, 37 | error: function(str){ 38 | console.error(color(' \033[31merror\033[0m \033[90m- %s\033[0m'), str); 39 | } 40 | }; 41 | 42 | return function(master){ 43 | 44 | // start 45 | master.on('start', function(){ 46 | log.info('master started'); 47 | }); 48 | 49 | // closing 50 | master.on('closing', function(){ 51 | log.info('shutting down'); 52 | }); 53 | 54 | // close 55 | master.on('close', function(){ 56 | log.info('shutdown complete'); 57 | }); 58 | 59 | // killing workers 60 | master.on('kill', function(sig){ 61 | log.warning('kill(' + (sig || 'SIGTERM') + ')'); 62 | }); 63 | 64 | // worker died 65 | master.on('worker killed', function(worker){ 66 | if ('restarting' == master.state) return; 67 | log.warning('worker ' + worker.id + ' died'); 68 | }); 69 | 70 | // worker exception 71 | master.on('worker exception', function(worker, err){ 72 | log.error('worker ' + worker.id + ' uncaught exception ' + err.message); 73 | }); 74 | 75 | // worker is waiting on connections to be closed 76 | master.on('worker waiting', function(worker, connections){ 77 | log.warning('worker ' + worker.id + ' waiting on ' + connections + ' connections'); 78 | }); 79 | 80 | // worker has timed out 81 | master.on('worker timeout', function(worker, timeout){ 82 | log.warning('worker ' + worker.id + ' timed out after ' + timeout + 'ms'); 83 | }); 84 | 85 | // connection 86 | master.on('worker connected', function(worker){ 87 | log.info('worker ' + worker.id + ' connected'); 88 | }); 89 | 90 | // removed 91 | master.on('worker removed', function(worker){ 92 | log.info('worker ' + worker.id + ' removed'); 93 | }); 94 | 95 | // worker 96 | master.on('worker', function(worker){ 97 | log.info('worker ' + worker.id + ' spawned'); 98 | }); 99 | 100 | // listening 101 | master.on('listening', function(){ 102 | log.info('listening for connections'); 103 | }); 104 | 105 | // cyclic or immediate restart 106 | master.on('cyclic restart', function(){ 107 | log.warning('cyclic restart detected, restarting in ' + master.options['restart timeout'] + 'ms'); 108 | }); 109 | 110 | // restart requested 111 | master.on('restarting', function(){ 112 | log.info('restart requested'); 113 | }); 114 | 115 | // restart complete 116 | master.on('restart', function(){ 117 | log.info('restart complete'); 118 | }); 119 | 120 | // exit 121 | process.on('exit', function(){ 122 | log.debug('exit'); 123 | }); 124 | } 125 | }; -------------------------------------------------------------------------------- /lib/plugins/logger.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Cluster - logger 4 | * Copyright (c) 2011 LearnBoost 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var fs = require('fs') 13 | , Log = require('log') 14 | , mkdir = require('mkdirp').mkdirp; 15 | 16 | /** 17 | * Enable stdout / stderr logs for both the master 18 | * process, as well as workers. 19 | * 20 | * These output to the given `dir`, or `./logs` 21 | * relative to the server's file. 22 | * 23 | * Examples: 24 | * 25 | * // log to ./logs 26 | * engine(server) 27 | * .use(engine.logger()) 28 | * .listen(3000); 29 | * 30 | * // log to ./app/logs 31 | * engine(server) 32 | * .use(engine.logger('./app/logs')) 33 | * .listen(3000); 34 | * 35 | * // log to /var/log/node 36 | * engine(server) 37 | * .use(engine.logger('/var/log/node')) 38 | * .listen(3000); 39 | * 40 | * @param {String} dir 41 | * @param {Number} level 42 | * @return {Function} 43 | * @api public 44 | */ 45 | 46 | module.exports = function(dir, level){ 47 | return function(master){ 48 | dir = master.resolve(dir || 'logs'); 49 | 50 | mkdir(dir, 0755, function(err){ 51 | if (err) throw err; 52 | // master log 53 | var stream = fs.createWriteStream(dir + '/master.log', { flags: 'a' }); 54 | var log = master.log = new Log(level || Log.INFO, stream); 55 | 56 | // master events 57 | master.on('start', function(){ 58 | log.info('master started'); 59 | }); 60 | 61 | // master is shutting down 62 | master.on('closing', function(){ 63 | log.warning('shutting down master'); 64 | }); 65 | 66 | // master has closed and performed cleanup 67 | master.on('close', function(){ 68 | log.info('shutdown complete'); 69 | }); 70 | 71 | // sending signal to all workers 72 | master.on('kill', function(sig){ 73 | log.warning('sent kill(%s) to all workers', sig); 74 | }); 75 | 76 | // worker was killed 77 | master.on('worker killed', function(worker){ 78 | if ('restarting' == master.state) return; 79 | log.error('worker %s died', worker.id); 80 | }); 81 | 82 | // worker exception 83 | master.on('worker exception', function(worker, err){ 84 | log.error('worker %s uncaught exception %s', worker.id, err.message); 85 | }); 86 | 87 | // worker is waiting on connections to be closed 88 | master.on('worker waiting', function(worker, connections){ 89 | log.info('worker %s waiting on %s connections', worker.id, connections); 90 | }); 91 | 92 | // worker has timed out 93 | master.on('worker timeout', function(worker, timeout){ 94 | log.warning('worker %s timed out after %sms', worker.id, timeout); 95 | }); 96 | 97 | // worker connected to master 98 | master.on('worker connected', function(worker){ 99 | log.debug('worker %s connected', worker.id); 100 | }); 101 | 102 | // cyclic or immediate restart 103 | master.on('cyclic restart', function(){ 104 | log.warning('cyclic restart detected, restarting in %sms' 105 | , master.options['restart timeout']); 106 | }); 107 | 108 | // restart requested 109 | master.on('restarting', function(){ 110 | log.info('restart requested'); 111 | }); 112 | 113 | // restart complete 114 | master.on('restart', function(){ 115 | log.info('restart complete'); 116 | }); 117 | 118 | // repl socket connection established 119 | master.on('repl socket', function(sock){ 120 | var from = sock.remoteAddress 121 | ? 'from ' + sock.remoteAddress 122 | : ''; 123 | sock.on('connect', function(){ 124 | log.info('repl connection %s', from); 125 | }); 126 | sock.on('close', function(){ 127 | log.info('repl disconnect %s', from); 128 | }); 129 | }); 130 | 131 | // override fds 132 | master.customFds = [-1, -1]; 133 | 134 | // children 135 | master.on('worker', function(worker){ 136 | var proc = worker.proc; 137 | 138 | log.info('spawned worker ' + worker.id); 139 | 140 | // worker log streams 141 | var access = fs.createWriteStream(dir + '/workers.access.log', { flags: 'a' }) 142 | , error = fs.createWriteStream(dir + '/workers.error.log', { flags: 'a' }); 143 | 144 | // redirect stdout / stderr 145 | proc.stdout.pipe(access); 146 | proc.stderr.pipe(error); 147 | }); 148 | }); 149 | } 150 | }; -------------------------------------------------------------------------------- /lib/plugins/pidfiles.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Cluster - pidfiles 4 | * Copyright (c) 2011 LearnBoost 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var fs = require('fs') 13 | , mkdir = require('mkdirp').mkdirp; 14 | 15 | /** 16 | * Save pidfiles to the given `dir` or `./pids`. 17 | * 18 | * Examples: 19 | * 20 | * // save to ./pids 21 | * cluster(server) 22 | * .use(cluster.pidfiles()) 23 | * .listen(3000); 24 | * 25 | * // save to /tmp 26 | * cluster(server) 27 | * .use(cluster.pidfiles('/tmp')) 28 | * .listen(3000); 29 | * 30 | * // save to /var/run/node 31 | * cluster(server) 32 | * .use(cluster.logger('/var/run/node')) 33 | * .listen(3000); 34 | * 35 | * @param {String} dir 36 | * @return {Function} 37 | * @api public 38 | */ 39 | 40 | module.exports = function(dir){ 41 | return function(master){ 42 | dir = master.pidfiles = master.resolve(dir || 'pids'); 43 | 44 | // augment master 45 | master.pidof = function(name){ 46 | var dir = master.pidfiles 47 | , path = dir + '/' + name + '.pid' 48 | , pid = fs.readFileSync(path, 'ascii'); 49 | 50 | return parseInt(pid, 10); 51 | }; 52 | 53 | master.workerpids = function(){ 54 | var dir = master.pidfiles; 55 | return fs.readdirSync(dir).filter(function(file){ 56 | return file.match(/^worker\./); 57 | }).map(function(file){ 58 | return parseInt(fs.readFileSync(dir + '/' + file, 'ascii'), 10); 59 | }); 60 | }; 61 | 62 | mkdir(dir, 0755, function(err){ 63 | if (err) throw err; 64 | 65 | // save worker pids 66 | master.on('worker', function(worker){ 67 | var path = dir + '/worker.' + worker.id + '.pid'; 68 | fs.writeFile(path, worker.proc.pid.toString(), 'ascii', function(err){ 69 | if (err) throw err; 70 | master.emit('worker pidfile'); 71 | }); 72 | }); 73 | 74 | master.on('listening', function(){ 75 | // save master pid 76 | fs.writeFile(dir + '/master.pid', process.pid.toString(), 'ascii', function(err){ 77 | if (err) throw err; 78 | master.emit('pidfile'); 79 | }); 80 | }); 81 | }); 82 | } 83 | }; -------------------------------------------------------------------------------- /lib/plugins/reload.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Cluster - reload 3 | * Copyright (c) 2011 LearnBoost 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var fs = require('fs') 12 | , basename = require('path').basename 13 | , extname = require('path').extname; 14 | 15 | /** 16 | * Restart the server the given js `files` have changed. 17 | * `files` may be several directories, filenames, etc, 18 | * defaulting to the server's root directory. 19 | * 20 | * Options: 21 | * 22 | * - `signal` Signal defaulting to __SIGTERM__ 23 | * - `interval` Watcher interval, defaulting to `100` 24 | * - `extensions` File extensions to watch, defaults to ['.js'] 25 | * 26 | * Examples: 27 | * 28 | * cluster(server) 29 | * .use(cluster.reload()) 30 | * .listen(3000); 31 | * 32 | * cluster(server) 33 | * .use(cluster.reload('lib')) 34 | * .listen(3000); 35 | * 36 | * cluster(server) 37 | * .use(cluster.reload(['lib', 'tests', 'index.js'])) 38 | * .listen(3000); 39 | * 40 | * cluster(server) 41 | * .use(cluster.reload('lib', { interval: 60000 })) 42 | * .listen(3000); 43 | * 44 | * cluster(server) 45 | * .use(cluster.reload('lib', { extensions: ['.js', '.coffee'] })) 46 | * .listen(3000); 47 | * 48 | * Ignore Directories: 49 | * 50 | * By default `reload()` will ignore the following directories: 51 | * 52 | * - node_modules 53 | * - support 54 | * - examples 55 | * - test 56 | * - bin 57 | * 58 | * Alter with `reload.ignoreDirectories` 59 | * 60 | * cluster.reload.ignoreDirectories.push('src'); 61 | * 62 | * @param {String|Array} files 63 | * @param {Options} options 64 | * @return {Function} 65 | * @api public 66 | */ 67 | 68 | exports = module.exports = function(files, options){ 69 | options = options || {}; 70 | 71 | // defaults 72 | var interval = options.interval || 100 73 | , extensions = options.extensions || ['.js'] 74 | , signal = options.signal || 'SIGTERM'; 75 | 76 | return function(master){ 77 | if (!files) files = master.dir; 78 | if (!Array.isArray(files)) files = [files]; 79 | files.forEach(traverse); 80 | 81 | // traverse file if it is a directory 82 | // otherwise setup the watcher 83 | function traverse(file) { 84 | file = master.resolve(file); 85 | fs.stat(file, function(err, stat){ 86 | if (!err) { 87 | if (stat.isDirectory()) { 88 | if (~exports.ignoreDirectories.indexOf(basename(file))) return; 89 | fs.readdir(file, function(err, files){ 90 | files.map(function(f){ 91 | return file + '/' + f; 92 | }).forEach(traverse); 93 | }); 94 | } else { 95 | watch(file); 96 | } 97 | } 98 | }); 99 | } 100 | 101 | // watch file for changes 102 | function watch(file) { 103 | if (!~extensions.indexOf(extname(file))) return; 104 | fs.watchFile(file, { interval: interval }, function(curr, prev){ 105 | if (curr.mtime > prev.mtime) { 106 | console.log(' \033[36mchanged\033[0m \033[90m- %s\033[0m', file); 107 | master.restartWorkers(signal); 108 | } 109 | }); 110 | } 111 | } 112 | }; 113 | 114 | /** 115 | * Directories to ignore. 116 | */ 117 | 118 | exports.ignoreDirectories = ['node_modules', 'support', 'test', 'bin']; -------------------------------------------------------------------------------- /lib/plugins/repl.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Cluster - repl 4 | * Copyright (c) 2011 LearnBoost 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var net = require('net') 13 | , repl = require('repl'); 14 | 15 | /** 16 | * Enable REPL with all arguments passed to `net.Server#listen()`. 17 | * 18 | * Examples: 19 | * 20 | * cluster(server) 21 | * .use(cluster.stats()) 22 | * .use(cluster.repl('/var/run/cluster')) 23 | * .listen(); 24 | * 25 | * In the terminal: 26 | * 27 | * $ sudo telnet /var/run/cluster 28 | * 29 | * @return {Function} 30 | * @api public 31 | */ 32 | 33 | exports = module.exports = function(){ 34 | var args = arguments; 35 | if (!args.length) throw new Error('repl() plugin requires port/host or path'); 36 | return function(master){ 37 | var server 38 | , sockets = []; 39 | 40 | 41 | // start repl 42 | function start(){ 43 | // TCP or unix-domain socket repl 44 | server = net.createServer(function(sock){ 45 | sockets.push(sock); 46 | var ctx = repl.start('cluster> ', sock).context; 47 | master.emit('repl socket', sock); 48 | 49 | // augment socket to provide some formatting methods 50 | sock.title = function(str){ this.write('\n \033[36m' + str + '\033[0m\n'); } 51 | sock.row = function(key, val){ this.write(' \033[90m' + key + ':\033[0m ' + val + '\n'); } 52 | 53 | // merge commands into context 54 | // executing in context of master 55 | Object.keys(exports).forEach(function(cmd){ 56 | ctx[cmd] = function(){ 57 | var args = Array.prototype.slice.call(arguments); 58 | args.unshift(master, sock); 59 | return exports[cmd].apply(master, args); 60 | }; 61 | }); 62 | }); 63 | 64 | // Apply all arguments given 65 | server.listen.apply(server, args); 66 | } 67 | 68 | // initial master starts immediately 69 | // replacements starts when the previous 70 | // has closed 71 | master.on(master.isChild 72 | ? 'restart' 73 | : 'start', start); 74 | 75 | // restart notification 76 | master.on('restarting', function(){ 77 | sockets.forEach(function(sock){ 78 | if (sock.fd) { 79 | sock.write('\n\033[33mrestarting\033[0m - closing connection soon\n'); 80 | } 81 | }); 82 | }); 83 | 84 | // close 85 | master.on('close', function(){ 86 | sockets.forEach(function(sock){ 87 | sock.fd && sock.end(); 88 | }); 89 | if (server && server.fd) server.close(); 90 | }); 91 | } 92 | }; 93 | 94 | /** 95 | * Define function `name`, with the given callback 96 | * `fn(master, sock, ...)` and `description`. 97 | * 98 | * @param {String} name 99 | * @param {Function} fn 100 | * @param {String} desc 101 | * @return {Object} exports for chaining 102 | * @api public 103 | */ 104 | 105 | var define = exports.define = function(name, fn, desc){ 106 | (exports[name] = fn).description = desc; 107 | return exports; 108 | }; 109 | 110 | /** 111 | * Display commmand help. 112 | */ 113 | 114 | define('help', function(master, sock){ 115 | sock.title('Commands'); 116 | Object.keys(exports).forEach(function(cmd){ 117 | if ('define' == cmd) return; 118 | 119 | var fn = exports[cmd] 120 | , params = fn.toString().match(/^function +\((.*?)\)/)[1] 121 | , params = params.split(/ *, */).slice(2); 122 | 123 | sock.row( 124 | cmd + '(' + params.join(', ') + ')' 125 | , fn.description); 126 | }); 127 | sock.write('\n'); 128 | }, 'Display help information'); 129 | 130 | /** 131 | * Spawn `n` additional workers with the given `signal`. 132 | */ 133 | 134 | define('spawn', function(master, sock, n, signal){ 135 | n = n || 1; 136 | if (n < 0) { 137 | n = Math.abs(n); 138 | sock.write('removing ' + n + ' worker' + (n > 1 ? 's' : '') 139 | + ' with ' + (signal || 'SIGQUIT') + '\n'); 140 | master.remove(n, signal); 141 | } else { 142 | sock.write('spawning ' + n + ' worker' + (n > 1 ? 's' : '') + '\n'); 143 | master.spawn(n); 144 | } 145 | }, 'Spawn one or more additional workers, or remove one or more'); 146 | 147 | /** 148 | * Output process ids. 149 | */ 150 | 151 | define('pids', function(master, sock){ 152 | sock.title('pids'); 153 | sock.row('master', process.pid); 154 | master.children.forEach(function(worker){ 155 | sock.row('worker #' + worker.id, worker.proc.pid); 156 | }); 157 | sock.write('\n'); 158 | }, 'Output process ids'); 159 | 160 | /** 161 | * Kill the given worker by `id` and `signal`. 162 | */ 163 | 164 | define('kill', function(master, sock, id, signal){ 165 | var worker = master.children[id]; 166 | if (worker) { 167 | worker.proc.kill(signal); 168 | sock.write('sent \033[36m' + (signal || 'SIGTERM') + '\033[0m to worker #' + id + '\n'); 169 | } else { 170 | sock.write('invalid worker id\n'); 171 | } 172 | }, 'Send signal or SIGTERM to the given worker'); 173 | 174 | /** 175 | * Gracefully shutdown. 176 | */ 177 | 178 | define('shutdown', function(master, sock){ 179 | master.close(); 180 | }, 'Gracefully shutdown server'); 181 | 182 | /** 183 | * Hard shutdown. 184 | */ 185 | 186 | define('stop', function(master, sock){ 187 | master.destroy(); 188 | }, 'Hard shutdown'); 189 | 190 | /** 191 | * Gracefully restart all workers. 192 | */ 193 | 194 | define('restart', function(master, sock){ 195 | master.restart(); 196 | }, 'Gracefully restart all workers'); -------------------------------------------------------------------------------- /lib/plugins/stats.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Cluster - stats 4 | * Copyright (c) 2011 LearnBoost 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var fs = require('fs') 13 | , Log = require('log') 14 | , repl = require('./repl') 15 | , utils = require('../utils') 16 | , os = require('os'); 17 | 18 | /** 19 | * Enable stat tracking with the given `options`. 20 | * 21 | * Options: 22 | * 23 | * - `connections` enable connection statistics 24 | * - `requests` enable request statistics 25 | * - `lightRequests` enable light-weight request statistics 26 | * 27 | * Real-time applications should utilize `lightRequests` for reporting 28 | * when possible, although less data is available. 29 | * 30 | * TODO: UDP 31 | * 32 | * @param {Object} options 33 | * @return {Function} 34 | * @api public 35 | */ 36 | 37 | module.exports = function(options){ 38 | options = options || {}; 39 | stats.enableInWorker = options.connections || options.requests; 40 | 41 | function stats(master){ 42 | var server = master.server; 43 | master.connectionStats = options.connections; 44 | master.requestStats = options.requests; 45 | master.lightRequestStats = options.lightRequests; 46 | 47 | // worker stats 48 | if (master.isWorker) { 49 | var id = 0; 50 | 51 | // connections 52 | if (options.connections) { 53 | server.on('connection', function(sock){ 54 | var data = { remoteAddress: sock.remoteAddress }; 55 | master.call('reportStats', 'connection', data); 56 | sock.on('close', function(){ 57 | master.call('reportStats', 'disconnection', data); 58 | }); 59 | }); 60 | } 61 | 62 | // light-weight requests 63 | if (options.lightRequests) { 64 | utils.unshiftListener(server, 'request', function(req, res){ 65 | master.call('reportStats', 'light request', res.id = ++id); 66 | var end = res.end; 67 | res.end = function(str, encoding){ 68 | res.end = end; 69 | res.end(str, encoding); 70 | master.call('reportStats', 'light request complete', res.id); 71 | }; 72 | }); 73 | } 74 | 75 | // requests 76 | if (options.requests) { 77 | utils.unshiftListener(server, 'request', function(req, res){ 78 | var data = { 79 | remoteAddress: req.socket.remoteAddress 80 | , headers: req.headers 81 | , httpVersion: req.httpVersion 82 | , method: req.method 83 | , url: req.url 84 | , id: ++id 85 | }; 86 | 87 | master.call('reportStats', 'request', data); 88 | 89 | var end = res.end; 90 | res.end = function(str, encoding){ 91 | res.end = end; 92 | res.end(str, encoding); 93 | master.call('reportStats', 'request complete', data); 94 | }; 95 | }); 96 | } 97 | // master stats 98 | } else { 99 | master.stats = { 100 | start: new Date 101 | , restarts: 0 102 | , workersSpawned: 0 103 | , workersKilled: 0 104 | }; 105 | 106 | // 0.4.x 107 | if (os) { 108 | master.stats.totalmem = os.totalmem(); 109 | master.stats.freemem = os.freemem(); 110 | } 111 | 112 | // worker stats 113 | master.reportStats = function(worker, type, data){ 114 | master.emit('client ' + type, worker, data); 115 | switch (type) { 116 | case 'connection': 117 | worker.stats.connectionsTotal++; 118 | worker.stats.connectionsActive++; 119 | break; 120 | case 'disconnection': 121 | worker.stats.connectionsActive--; 122 | break; 123 | case 'light request': 124 | case 'request': 125 | worker.stats.requestsTotal++; 126 | } 127 | }; 128 | 129 | // total workers spawned 130 | master.on('worker', function(worker){ 131 | ++master.stats.workersSpawned; 132 | worker.stats = { 133 | start: new Date 134 | , connectionsTotal: 0 135 | , connectionsActive: 0 136 | , requestsTotal: 0 137 | }; 138 | }); 139 | 140 | // total worker deaths 141 | master.on('worker killed', function(worker){ 142 | ++master.stats.workersKilled; 143 | }); 144 | 145 | // restarting 146 | master.on('restarting', function(data){ 147 | ++master.stats.restarts; 148 | data.stats = master.stats; 149 | }); 150 | 151 | // restart 152 | master.on('restart', function(data){ 153 | master.stats = data.stats; 154 | master.stats.start = new Date(master.stats.start); 155 | }); 156 | } 157 | } 158 | 159 | return stats; 160 | }; 161 | 162 | /** 163 | * REPL statistics command. 164 | */ 165 | 166 | repl.define('stats', function(master, sock){ 167 | var active = master.children.length 168 | , total = master.stats.workersSpawned 169 | , deaths = master.stats.workersKilled 170 | , restarts = master.stats.restarts; 171 | 172 | // master stats 173 | sock.title('Master'); 174 | if (os) sock.row('os', os.type() + ' ' + os.release()); 175 | sock.row('state', master.state); 176 | sock.row('started', master.stats.start.toUTCString()); 177 | sock.row('uptime', utils.formatDateRange(new Date, master.stats.start)); 178 | sock.row('restarts', restarts); 179 | sock.row('workers', active); 180 | sock.row('deaths', deaths); 181 | 182 | // resources 183 | if (os) { 184 | sock.title('Resources'); 185 | sock.row('load average', os.loadavg().map(function(n){ return n.toFixed(2); }).join(' ')); 186 | sock.row('cores utilized', active + ' / ' + os.cpus().length); 187 | var free = utils.formatBytes(master.stats.freemem); 188 | var total = utils.formatBytes(master.stats.totalmem); 189 | sock.row('memory at boot (free / total)', free + ' / ' + total); 190 | var free = utils.formatBytes(os.freemem()); 191 | var total = utils.formatBytes(os.totalmem()); 192 | sock.row('memory now (free / total)', free + ' / ' + total); 193 | } 194 | 195 | // worker stats 196 | sock.title('Workers'); 197 | 198 | // connections 199 | if (master.connectionStats) { 200 | sock.row('connections total', sum(master.children, 'connectionsTotal')); 201 | sock.row('connections active', sum(master.children, 'connectionsActive')); 202 | } 203 | 204 | // requests 205 | if (master.requestStats) { 206 | sock.row('requests total', sum(master.children, 'requestsTotal')); 207 | } 208 | 209 | master.children.forEach(function(worker){ 210 | var stats = '' 211 | , piped = []; 212 | 213 | // uptime 214 | stats += utils.formatDateRange(new Date, worker.stats.start); 215 | 216 | // connections 217 | if (master.connectionStats) { 218 | piped.push(worker.stats.connectionsActive); 219 | piped.push(worker.stats.connectionsTotal); 220 | } 221 | 222 | // requests 223 | if (master.requestStats) { 224 | piped.push(worker.stats.requestsTotal); 225 | } 226 | 227 | if (piped.length) { 228 | stats += ' ' + piped.join('\033[90m|\033[0m'); 229 | } 230 | 231 | sock.row(worker.id, stats); 232 | }); 233 | sock.write('\n'); 234 | }, 'Display server statistics'); 235 | 236 | 237 | /** 238 | * Return sum of each `prop` in `arr`. 239 | * 240 | * @param {Array} arr 241 | * @param {String} prop 242 | * @return {Number} 243 | * @api private 244 | */ 245 | 246 | function sum(arr, prop){ 247 | return arr.reduce(function(sum, obj){ 248 | return sum + obj.stats[prop]; 249 | }, 0); 250 | }; 251 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Cluster - utils 4 | * Copyright (c) 2011 LearnBoost 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Frame the given `obj`. 10 | * 11 | * @param {Object} obj 12 | * @return {String} 13 | * @api private 14 | */ 15 | 16 | exports.frame = function(obj){ 17 | return JSON.stringify(obj) + '\n'; 18 | }; 19 | 20 | /** 21 | * Fast alternative to `Array.prototype.slice.call()`. 22 | * 23 | * @param {Arguments} args 24 | * @param {Number} index 25 | * @return {Array} 26 | * @api private 27 | */ 28 | 29 | exports.toArray = function(args, index){ 30 | var arr = [] 31 | , len = args.length; 32 | for (var i = (index || 0); i < len; ++i) { 33 | arr.push(args[i]); 34 | } 35 | return arr; 36 | }; 37 | 38 | /** 39 | * Format byte-size. 40 | * 41 | * @param {Number} bytes 42 | * @return {String} 43 | * @api private 44 | */ 45 | 46 | exports.formatBytes = function(bytes) { 47 | var kb = 1024 48 | , mb = 1024 * kb 49 | , gb = 1024 * mb; 50 | if (bytes < kb) return bytes + 'b'; 51 | if (bytes < mb) return (bytes / kb).toFixed(2) + 'kb'; 52 | if (bytes < gb) return (bytes / mb).toFixed(2) + 'mb'; 53 | return (bytes / gb).toFixed(2) + 'gb'; 54 | }; 55 | 56 | /** 57 | * Format date difference between `a` and `b`. 58 | * 59 | * @param {Date} a 60 | * @param {Date} b 61 | * @return {String} 62 | * @api private 63 | */ 64 | 65 | exports.formatDateRange = function(a, b) { 66 | var diff = a > b ? a - b : b - a 67 | , second = 1000 68 | , minute = second * 60 69 | , hour = minute * 60 70 | , day = hour * 24; 71 | 72 | function unit(name, n) { 73 | return n + ' ' + name + (1 == n ? '' : 's'); 74 | } 75 | 76 | if (diff < second) return unit('millisecond', diff); 77 | if (diff < minute) return unit('second', (diff / second).toFixed(0)); 78 | if (diff < hour) return unit('minute', (diff / minute).toFixed(0)); 79 | if (diff < day) return unit('hour', (diff / hour).toFixed(0)); 80 | return unit('day', (diff / day).toFixed(1)); 81 | }; 82 | 83 | /** 84 | * Unshift a callback. 85 | * 86 | * @param {Object} obj 87 | * @param {String} event 88 | * @param {String} fn 89 | * @api private 90 | */ 91 | 92 | exports.unshiftListener = function(obj, event, fn){ 93 | if (Array.isArray(obj._events[event])) { 94 | obj._events[event].unshift(fn); 95 | } else { 96 | obj._events[event] = [fn, obj._events[event]]; 97 | } 98 | }; -------------------------------------------------------------------------------- /lib/worker.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Cluster - Worker 4 | * Copyright(c) 2011 LearnBoost 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var EventEmitter = require('events').EventEmitter 13 | , spawn = require('child_process').spawn 14 | , binding = process.binding('net') 15 | , utils = require('./utils') 16 | , net = require('net'); 17 | 18 | /** 19 | * Node binary. 20 | */ 21 | 22 | var node = process.execPath; 23 | 24 | /** 25 | * Initialize a new `Worker` with the given `master`. 26 | * 27 | * Signals: 28 | * 29 | * - `SIGINT` immediately exit 30 | * - `SIGTERM` immediately exit 31 | * - `SIGQUIT` graceful exit 32 | * 33 | * @param {Master} master 34 | * @api private 35 | */ 36 | 37 | var Worker = module.exports = function Worker(master) { 38 | this.master = master; 39 | this.server = master.server; 40 | }; 41 | 42 | /** 43 | * Inherit from `EventEmitter.prototype`. 44 | */ 45 | 46 | Worker.prototype.__proto__ = EventEmitter.prototype; 47 | 48 | /** 49 | * Worker is a receiver. 50 | */ 51 | 52 | require('./mixins/receiver')(Worker.prototype); 53 | 54 | /** 55 | * Start worker. 56 | * 57 | * @api private 58 | */ 59 | 60 | Worker.prototype.start = function(){ 61 | var self = this 62 | , call = this.master.call; 63 | 64 | // proxy to provide worker id 65 | this.master.call = function(){ 66 | var args = utils.toArray(arguments); 67 | // Allow calling master methods that 68 | // don't take worker as first argument 69 | if (false !== args[0]) args.unshift(self.id); 70 | return call.apply(this, args); 71 | }; 72 | 73 | // stdin 74 | this.stdin = new net.Socket(0, 'unix'); 75 | this.stdin.setEncoding('ascii'); 76 | this.stdin.on('data', this.frame.bind(this)); 77 | this.stdin.resume(); 78 | 79 | // demote usr/group 80 | if (this.server && this.server.listenFD) { 81 | this.server.on('listening', function(){ 82 | var group = self.options.group; 83 | if (group) process.setgid(group); 84 | var user = self.options.user; 85 | if (user) process.setuid(user); 86 | }); 87 | 88 | // stdin 89 | this.stdin.on('fd', this.server.listenFD.bind(this.server)); 90 | } 91 | 92 | // signal handlers 93 | process.on('SIGINT', this.destroy.bind(this)); 94 | process.on('SIGTERM', this.destroy.bind(this)); 95 | process.on('SIGQUIT', this.close.bind(this)); 96 | 97 | // conditionally handle uncaughtException 98 | process.nextTick(function(){ 99 | if (!process.listeners('uncaughtException').length) { 100 | process.on('uncaughtException', function(err){ 101 | // stderr for logs 102 | console.error(err.stack || err.message); 103 | 104 | // report exception 105 | self.master.call('workerException', err); 106 | 107 | // exit 108 | process.nextTick(function(){ 109 | self.destroy(); 110 | }); 111 | }); 112 | } 113 | }); 114 | }; 115 | 116 | /** 117 | * Received connect event, set the worker `id` 118 | * and `options`. 119 | * 120 | * @param {String} id 121 | * @param {Object} options 122 | * @api private 123 | */ 124 | 125 | Worker.prototype.connect = function(id, options){ 126 | this.options = options; 127 | 128 | // worker id 129 | this.id = id; 130 | 131 | // timeout 132 | this.timeout = options.timeout; 133 | 134 | // title 135 | process.title = options['worker title'].replace('{n}', id); 136 | 137 | // notify master of connection 138 | this.master.call('connect'); 139 | }; 140 | 141 | /** 142 | * Immediate shutdown. 143 | * 144 | * @api private 145 | */ 146 | 147 | Worker.prototype.destroy = function(){ 148 | this.emit('close'); 149 | process.nextTick(process.exit); 150 | }; 151 | 152 | /** 153 | * Perform graceful shutdown. 154 | * 155 | * @api private 156 | */ 157 | 158 | Worker.prototype.close = function(){ 159 | var self = this 160 | , server = this.server; 161 | 162 | if (server && server.connections) { 163 | // stop accepting 164 | server.watcher.stop(); 165 | 166 | // check pending connections 167 | setInterval(function(){ 168 | self.master.call('workerWaiting', server.connections); 169 | server.connections || self.destroy(); 170 | }, 2000); 171 | 172 | // timeout 173 | if (this.timeout) { 174 | setTimeout(function(){ 175 | self.master.call('workerTimeout', self.timeout); 176 | self.destroy(); 177 | }, this.timeout); 178 | } 179 | } else { 180 | this.destroy(); 181 | } 182 | }; 183 | 184 | /** 185 | * Spawn the worker with the given `id`. 186 | * 187 | * @param {Number} id 188 | * @return {Worker} for chaining 189 | * @api private 190 | */ 191 | 192 | Worker.prototype.spawn = function(id){ 193 | var fds = binding.socketpair() 194 | , customFds = [fds[0]].concat(this.master.customFds) 195 | , env = {}; 196 | 197 | // merge env 198 | for (var key in process.env) { 199 | env[key] = process.env[key]; 200 | } 201 | 202 | this.id = env.CLUSTER_WORKER = id; 203 | 204 | // spawn worker process 205 | this.proc = spawn( 206 | node 207 | , this.master.cmd 208 | , { customFds: customFds, env: env }); 209 | 210 | // unix domain socket for ICP + fd passing 211 | this.sock = new net.Socket(fds[1], 'unix'); 212 | 213 | // saving file descriptors for later use 214 | this.fds = fds; 215 | 216 | return this; 217 | }; 218 | 219 | /** 220 | * Invoke worker's `method` (called from Master). 221 | * 222 | * @param {String} method 223 | * @param {...} args 224 | * @api private 225 | */ 226 | 227 | Worker.prototype.call = function(method){ 228 | this.sock.write(utils.frame({ 229 | args: utils.toArray(arguments, 1) 230 | , method: method 231 | }), 'ascii'); 232 | }; 233 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cluster", 3 | "description": "extensible multi-core server manager", 4 | "keywords": ["server", "spark", "fugue", "tcp", "workers"], 5 | "version": "0.7.7", 6 | "homepage": "http://learnboost.github.com/cluster", 7 | "repository": "git://github.com/LearnBoost/cluster.git", 8 | "author": "TJ Holowaychuk (http://tjholowaychuk.com)", 9 | "main": "index", 10 | "dependencies": { 11 | "log": ">= 1.2.0" 12 | , "mkdirp": ">= 0.0.1" 13 | }, 14 | "devDependencies": { "should": "0.2.x" }, 15 | "engines": { "node": "*" } 16 | } -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var http = require('http') 7 | , should = require('should'); 8 | -------------------------------------------------------------------------------- /test/logs/.gitignore: -------------------------------------------------------------------------------- 1 | *.log -------------------------------------------------------------------------------- /test/logs/nested/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearnBoost/cluster/0d045131963e7a12ef19e94e0e35653b2dc5bd5e/test/logs/nested/.gitignore -------------------------------------------------------------------------------- /test/pids/.gitignore: -------------------------------------------------------------------------------- 1 | *.pid 2 | -------------------------------------------------------------------------------- /test/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -f test/pids/*.pid 4 | rm -f test/*.sock test/support/*.sock 5 | rm -f test/logs/*.log 6 | rm -f test/logs/nested/*.log 7 | 8 | clean() { 9 | killall -KILL node &> /dev/null 10 | } 11 | 12 | clean 13 | echo 14 | 15 | files=test/test.*.js 16 | for file in $files; do 17 | printf "\033[90m ${file#test/}\033[0m " 18 | node $@ $file && echo "\033[36m✓\033[0m" 19 | test $? -eq 0 || exit $? 20 | done 21 | echo -------------------------------------------------------------------------------- /test/support/all.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../../') 7 | , should = require('should') 8 | , http = require('http') 9 | , fs = require('fs'); 10 | 11 | require('../common'); 12 | 13 | var server = http.createServer(function(req, res){ 14 | setTimeout(function(){ 15 | res.writeHead(200); 16 | res.end('Hello World'); 17 | }, 1000); 18 | }); 19 | 20 | cluster = cluster(server) 21 | .set('workers', 6) 22 | .use(cluster.pidfiles()) 23 | .use(cluster.cli()) 24 | .use(cluster.logger(__dirname + '/../logs')) 25 | .use(cluster.repl(8888, 'localhost')) 26 | .use(cluster.stats()) 27 | .listen(3000); 28 | 29 | cluster.on('listening', function(){ 30 | console.log('listening'); 31 | }); -------------------------------------------------------------------------------- /test/support/exported.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var http = require('http'); 7 | 8 | module.exports = http.createServer(function(req, res){ 9 | res.writeHead(200); 10 | res.end('Hello World'); 11 | }); -------------------------------------------------------------------------------- /test/support/logs/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | -------------------------------------------------------------------------------- /test/support/pids/.gitignore: -------------------------------------------------------------------------------- 1 | *.pid 2 | -------------------------------------------------------------------------------- /test/support/server.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../../') 7 | , should = require('should') 8 | , http = require('http') 9 | , fs = require('fs'); 10 | 11 | require('../common'); 12 | 13 | var server = http.createServer(function(req, res){ 14 | setTimeout(function(){ 15 | res.writeHead(200); 16 | res.end('Hello World'); 17 | }, 1000); 18 | }); 19 | 20 | cluster = cluster(server) 21 | .set('workers', 2) 22 | .listen(3000); 23 | 24 | cluster.on('listening', function(){ 25 | console.log('listening'); 26 | }); -------------------------------------------------------------------------------- /test/support/standalone.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/test.basic.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | require('./common'); 10 | 11 | var server = http.createServer(function(req, res){ 12 | res.writeHead(200); 13 | res.end('Hello World'); 14 | }); 15 | 16 | cluster = cluster(server) 17 | .listen(3000); 18 | 19 | cluster.on('listening', function(){ 20 | process.cwd().should.include.string('cluster/test'); 21 | http.get({ host: 'localhost', port: 3000 }, function(res){ 22 | res.on('data', function(chunk){ 23 | chunk.toString().should.equal('Hello World'); 24 | }); 25 | res.on('end', function(){ 26 | cluster.close(); 27 | }); 28 | }); 29 | }); -------------------------------------------------------------------------------- /test/test.cli-status.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http') 8 | , fs = require('fs') 9 | , Master = require('../lib/master.js'); 10 | 11 | require('./common'); 12 | 13 | var server = http.createServer(function(req, res){ 14 | setTimeout(function(){ 15 | res.writeHead(200); 16 | res.end('Hello World'); 17 | }, 1000); 18 | }); 19 | 20 | cluster = cluster(server) 21 | .set('workers', 1) 22 | .use(cluster.pidfiles()) 23 | .use(cluster.cli()) 24 | .in('development').listen(3000) 25 | .in('staging').listen(3010); 26 | 27 | 28 | cluster.on('listening', function(){ 29 | cluster.preventDefault = true; 30 | cluster.start().should.be.an.instanceof(Master); 31 | cluster.close(); 32 | }); 33 | -------------------------------------------------------------------------------- /test/test.dns.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | require('./common'); 10 | 11 | var server = http.createServer(function(req, res){ 12 | res.writeHead(200); 13 | res.end('Hello World'); 14 | }); 15 | 16 | cluster = cluster(server) 17 | .listen(3000, 'localhost'); 18 | 19 | cluster.on('listening', function(){ 20 | process.cwd().should.include.string('cluster/test'); 21 | http.get({ host: 'localhost', port: 3000 }, function(res){ 22 | res.on('data', function(chunk){ 23 | chunk.toString().should.equal('Hello World'); 24 | }); 25 | res.on('end', function(){ 26 | cluster.close(); 27 | }); 28 | }); 29 | }); -------------------------------------------------------------------------------- /test/test.env.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , assert = require('assert') 8 | , http = require('http'); 9 | 10 | require('./common'); 11 | 12 | var server = http.createServer(function(req, res){ 13 | res.writeHead(200); 14 | res.end('Hello World'); 15 | }); 16 | 17 | cluster = cluster(server) 18 | .listen(3000); 19 | 20 | if (cluster.isMaster) { 21 | process.env.FOO = 'bar'; 22 | assert.ok(!process.env.CLUSTER_WORKER); 23 | } else { 24 | process.env.FOO.should.equal('bar'); 25 | assert.ok(process.env.CLUSTER_WORKER); 26 | } 27 | 28 | cluster.on('listening', function(){ 29 | cluster.close(); 30 | }); -------------------------------------------------------------------------------- /test/test.ephemeral.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http') 8 | , app = http.createServer(); 9 | 10 | cluster(app) 11 | .set('workers', 2) 12 | .listen(0) 13 | .on('listening', process.exit); 14 | 15 | -------------------------------------------------------------------------------- /test/test.filename.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | require('./common'); 10 | 11 | 12 | cluster = cluster('support/exported') 13 | .set('workers', 4) 14 | .listen(3000); 15 | 16 | cluster.on('listening', function(){ 17 | process.cwd().should.include.string('cluster/test'); 18 | http.get({ host: 'localhost', port: 3000 }, function(res){ 19 | res.on('data', function(chunk){ 20 | chunk.toString().should.equal('Hello World'); 21 | }); 22 | res.on('end', function(){ 23 | cluster.close(); 24 | }); 25 | }); 26 | }); -------------------------------------------------------------------------------- /test/test.logger.custom-path.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http') 8 | , fs = require('fs'); 9 | 10 | require('./common'); 11 | 12 | var server = http.createServer(function(req, res){ 13 | console.log('%s %s', req.method, req.url); 14 | res.writeHead(200); 15 | res.end('Hello World'); 16 | }); 17 | 18 | cluster = cluster(server) 19 | .set('workers', 1) 20 | .use(cluster.logger(__dirname + '/logs/nested')) 21 | .listen(3000); 22 | 23 | cluster.on('listening', function(){ 24 | http.get({ host: 'localhost', port: 3000 }, function(res){ 25 | res.on('end', function(){ 26 | var files = fs.readdirSync(__dirname + '/logs/nested'); 27 | files.should.have.length(4); 28 | files.should.contain('master.log'); 29 | files.should.contain('workers.access.log'); 30 | files.should.contain('workers.error.log'); 31 | cluster.close(); 32 | }); 33 | }); 34 | }); -------------------------------------------------------------------------------- /test/test.logger.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http') 8 | , fs = require('fs'); 9 | 10 | require('./common'); 11 | 12 | var server = http.createServer(function(req, res){ 13 | console.log('%s %s', req.method, req.url); 14 | res.writeHead(200); 15 | res.end('Hello World'); 16 | }); 17 | 18 | cluster = cluster(server) 19 | .set('workers', 1) 20 | .use(cluster.logger()) 21 | .listen(3000); 22 | 23 | cluster.on('listening', function(){ 24 | http.get({ host: 'localhost', port: 3000 }, function(res){ 25 | res.on('end', function(){ 26 | var files = fs.readdirSync(__dirname + '/logs'); 27 | files.should.have.length(5); 28 | files.should.contain('master.log'); 29 | files.should.contain('workers.access.log'); 30 | files.should.contain('workers.error.log'); 31 | fs.readFile(__dirname + '/logs/workers.access.log', 'ascii', function(err, str){ 32 | str.should.match(/^GET \//); 33 | cluster.close(); 34 | }); 35 | }); 36 | }); 37 | }); -------------------------------------------------------------------------------- /test/test.pidfiles.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http') 8 | , fs = require('fs'); 9 | 10 | require('./common'); 11 | 12 | var server = http.createServer(function(req, res){ 13 | setTimeout(function(){ 14 | res.writeHead(200); 15 | res.end('Hello World'); 16 | }, 1000); 17 | }); 18 | 19 | cluster = cluster(server) 20 | .set('workers', 2) 21 | .use(cluster.pidfiles()) 22 | .listen(3000); 23 | 24 | function checkFile(name) { 25 | var pid = fs.readFileSync(__dirname + '/pids/' + name, 'ascii'); 26 | (!isNaN(parseInt(pid, 10))).should.be.true; 27 | } 28 | 29 | cluster.on('listening', function(){ 30 | fs.readdir(__dirname + '/pids', function(err, files){ 31 | // TODO: test master pid 32 | files.should.contain('worker.0.pid'); 33 | files.should.contain('worker.1.pid'); 34 | checkFile('worker.0.pid'); 35 | checkFile('worker.1.pid'); 36 | cluster.close(); 37 | }); 38 | }); -------------------------------------------------------------------------------- /test/test.restart-env.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http') 8 | , assert = require('assert'); 9 | 10 | require('./common'); 11 | 12 | var server = http.createServer(function(req, res){ 13 | res.writeHead(200); 14 | res.end('Hello World'); 15 | }); 16 | 17 | cluster = cluster(server) 18 | .listen(3001); 19 | 20 | if (cluster.isChild) { 21 | process.cwd().should.include.string('cluster/test'); 22 | cluster.on('listening', function(){ 23 | assert.equal(require.paths.join(':'), process.env.REQUIRE_PATHS); 24 | assert.equal(process.env.FOO, 'bar'); 25 | cluster.close(); 26 | }); 27 | } else { 28 | process.env.REQUIRE_PATHS = require.paths.join(':'); 29 | process.env.FOO = 'bar'; 30 | cluster.on('listening', function(){ 31 | cluster.restart(); 32 | }); 33 | } 34 | 35 | -------------------------------------------------------------------------------- /test/test.restart.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http') 8 | , fs = require('fs'); 9 | 10 | require('./common'); 11 | 12 | var server = http.createServer(function(req, res){ 13 | setTimeout(function(){ 14 | res.writeHead(200); 15 | res.end('Hello World'); 16 | }, 1000); 17 | }); 18 | 19 | cluster = cluster(server) 20 | .set('workers', 2) 21 | .use(cluster.pidfiles()) 22 | .listen(3002); 23 | 24 | var a, b 25 | , options = { host: 'localhost', port: 3002 }; 26 | 27 | function getPID(name) { 28 | var pid = fs.readFileSync(__dirname + '/pids/' + name, 'ascii'); 29 | return parseInt(pid, 10); 30 | } 31 | 32 | function movePID(name) { 33 | var pid = getPID(name); 34 | fs.writeFileSync(__dirname + '/pids/old.' + name, pid.toString(), 'ascii'); 35 | } 36 | 37 | if (cluster.isChild) { 38 | cluster.on('restart', function(){ 39 | http.get(options, function(res){ 40 | res.statusCode.should.equal(200); 41 | var a = getPID('old.worker.0.pid') 42 | , b = getPID('old.worker.1.pid'); 43 | a.should.not.equal(getPID('worker.0.pid')); 44 | b.should.not.equal(getPID('worker.1.pid')); 45 | cluster.close(); 46 | }); 47 | }); 48 | } else { 49 | var pending = 2; 50 | cluster.on('worker pidfile', function(){ 51 | --pending || (function(){ 52 | movePID('worker.0.pid') 53 | movePID('worker.1.pid'); 54 | 55 | // issue some requests 56 | var n = 20 57 | , pending = n; 58 | while (n--) { 59 | http.get(options, function(res){ 60 | res.statusCode.should.equal(200); 61 | --pending || cluster.restart(); 62 | }); 63 | } 64 | })(); 65 | }); 66 | } -------------------------------------------------------------------------------- /test/test.shutdown-all.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var spawn = require('child_process').spawn 7 | , http = require('http'); 8 | 9 | require('./common'); 10 | 11 | var calls = 0; 12 | 13 | // child process 14 | 15 | var child = spawn('node', [__dirname + '/support/all.js'], { 16 | customFds: [-1, -1, 2] 17 | }); 18 | 19 | // listening 20 | 21 | child.stdout.on('data', function(chunk){ 22 | var options = { host: 'localhost', port: 3000 }; 23 | 24 | http.get(options, function(res){ 25 | ++calls; 26 | res.statusCode.should.equal(200); 27 | child.kill('SIGQUIT'); 28 | }); 29 | 30 | http.get(options, function(res){ 31 | ++calls; 32 | res.statusCode.should.equal(200); 33 | }); 34 | 35 | http.get(options, function(res){ 36 | ++calls; 37 | res.statusCode.should.equal(200); 38 | }); 39 | 40 | http.get(options, function(res){ 41 | ++calls; 42 | res.statusCode.should.equal(200); 43 | }); 44 | }); 45 | 46 | child.on('exit', function(){ 47 | calls.should.equal(4); 48 | }); -------------------------------------------------------------------------------- /test/test.shutdown.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var spawn = require('child_process').spawn 7 | , http = require('http'); 8 | 9 | require('./common'); 10 | 11 | var calls = 0; 12 | 13 | // child process 14 | 15 | var child = spawn('node', [__dirname + '/support/server.js'], { 16 | customFds: [-1, -1, 2] 17 | }); 18 | 19 | // listening 20 | 21 | child.stdout.on('data', function(chunk){ 22 | var options = { host: 'localhost', port: 3000 }; 23 | 24 | http.get(options, function(res){ 25 | ++calls; 26 | res.statusCode.should.equal(200); 27 | child.kill('SIGQUIT'); 28 | }); 29 | 30 | http.get(options, function(res){ 31 | ++calls; 32 | res.statusCode.should.equal(200); 33 | }); 34 | 35 | http.get(options, function(res){ 36 | ++calls; 37 | res.statusCode.should.equal(200); 38 | }); 39 | 40 | http.get(options, function(res){ 41 | ++calls; 42 | res.statusCode.should.equal(200); 43 | }); 44 | }); 45 | 46 | child.on('exit', function(){ 47 | calls.should.equal(4); 48 | }); -------------------------------------------------------------------------------- /test/test.standalone-shutdown.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../'); 7 | 8 | require('./common'); 9 | 10 | cluster = cluster() 11 | .set('workers', 1) 12 | .start(); 13 | 14 | if (cluster.isWorker) { 15 | setTimeout(function(){ 16 | process.kill(process.env.CLUSTER_MASTER_PID, 'SIGQUIT'); 17 | }, 500); 18 | } -------------------------------------------------------------------------------- /test/test.standalone.file.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../'); 7 | 8 | cluster = cluster('./support/standalone') 9 | .set('workers', 4) 10 | .start(); 11 | 12 | if (cluster.isMaster) { 13 | setTimeout(function(){ 14 | process.kill(process.pid, 'SIGQUIT'); 15 | }, 500); 16 | } -------------------------------------------------------------------------------- /test/test.standalone.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../'); 7 | 8 | require('./common'); 9 | 10 | cluster = cluster() 11 | .set('workers', 2) 12 | .start(); 13 | 14 | if (cluster.isWorker) { 15 | setTimeout(process.exit, Math.random() * 300); 16 | } else { 17 | // make sure workers are re-spawned 18 | var n = 12; 19 | cluster.on('worker killed', function(worker){ 20 | --n || process.exit(); 21 | }); 22 | } -------------------------------------------------------------------------------- /test/test.standalone.restart.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../'); 7 | 8 | require('./common'); 9 | var assert = require('assert'); 10 | 11 | cluster = cluster() 12 | .set('workers', 2) 13 | .set('restart threshold', 0) 14 | .start(); 15 | 16 | if (!cluster.isMaster) return; 17 | var ppid = process.env.CLUSTER_PARENT_PID; 18 | 19 | cluster.on('listening', function(){ 20 | if (ppid) return cluster.close(); 21 | cluster.restart(); 22 | setTimeout(function() { 23 | throw new Error('Failed to kill parent master'); 24 | }, 3000); 25 | }); 26 | -------------------------------------------------------------------------------- /test/test.worker-kill.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | require('./common'); 10 | 11 | var server = http.createServer(function(req, res){ 12 | res.writeHead(200); 13 | res.end('Hello World'); 14 | }); 15 | 16 | cluster = cluster(server) 17 | .set('workers', 1) 18 | .listen(3000); 19 | 20 | cluster.on('listening', function(){ 21 | http.get({ host: 'localhost', port: 3000 }, function(res){ 22 | res.statusCode.should.equal(200); 23 | 24 | // kill the worker 25 | var pid = cluster.children[0].proc.pid; 26 | process.kill(pid, 'SIGKILL'); 27 | }); 28 | }); 29 | 30 | cluster.on('worker killed', function(worker){ 31 | worker.id.should.equal(0); 32 | http.get({ host: 'localhost', port: 3000 }, function(res){ 33 | res.statusCode.should.equal(200); 34 | cluster.close(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/test.worker-quit-keep-alive.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | require('./common'); 10 | 11 | var server = http.createServer(function(req, res){ 12 | res.writeHead(200); 13 | res.end('Hello World'); 14 | }); 15 | 16 | // request options 17 | 18 | var options = { 19 | host: 'localhost' 20 | , port: 3000 21 | , headers: { Connection: 'keep-alive' } 22 | }; 23 | 24 | // cluster 25 | 26 | cluster = cluster(server) 27 | .set('workers', 1) 28 | .set('timeout', 1000) 29 | .listen(3000); 30 | 31 | cluster.on('listening', function(){ 32 | http.get(options, function(res){ 33 | res.statusCode.should.equal(200); 34 | 35 | // kill the worker 36 | var pid = cluster.children[0].proc.pid; 37 | process.kill(pid, 'SIGQUIT'); 38 | }); 39 | }); 40 | 41 | var timeout; 42 | 43 | cluster.on('worker timeout', function(worker){ 44 | worker.id.should.equal(0); 45 | timeout = true; 46 | }); 47 | 48 | cluster.on('worker connected', function(worker){ 49 | if (timeout) { 50 | worker.id.should.equal(0); 51 | http.get(options, function(res){ 52 | res.statusCode.should.equal(200); 53 | cluster.close(); 54 | }); 55 | } 56 | }); -------------------------------------------------------------------------------- /test/test.worker-quit.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | require('./common'); 10 | 11 | var server = http.createServer(function(req, res){ 12 | res.writeHead(200); 13 | res.end('Hello World'); 14 | }); 15 | 16 | cluster = cluster(server) 17 | .set('workers', 1) 18 | .listen(3000); 19 | 20 | cluster.on('listening', function(){ 21 | http.get({ host: 'localhost', port: 3000 }, function(res){ 22 | res.statusCode.should.equal(200); 23 | 24 | // kill the worker 25 | var pid = cluster.children[0].proc.pid; 26 | process.kill(pid, 'SIGQUIT'); 27 | }); 28 | }); 29 | 30 | cluster.on('worker killed', function(worker){ 31 | worker.id.should.equal(0); 32 | http.get({ host: 'localhost', port: 3000 }, function(res){ 33 | res.statusCode.should.equal(200); 34 | cluster.close(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/test.worker-term.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | require('./common'); 10 | 11 | var server = http.createServer(function(req, res){ 12 | res.writeHead(200); 13 | res.end('Hello World'); 14 | }); 15 | 16 | cluster = cluster(server) 17 | .set('workers', 1) 18 | .listen(3000); 19 | 20 | cluster.on('listening', function(){ 21 | http.get({ host: 'localhost', port: 3000 }, function(res){ 22 | res.statusCode.should.equal(200); 23 | 24 | // kill the worker 25 | var pid = cluster.children[0].proc.pid; 26 | process.kill(pid, 'SIGTERM'); 27 | }); 28 | }); 29 | 30 | cluster.on('worker killed', function(worker){ 31 | worker.id.should.equal(0); 32 | http.get({ host: 'localhost', port: 3000 }, function(res){ 33 | res.statusCode.should.equal(200); 34 | cluster.close(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/test.working-dir.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var cluster = require('../') 7 | , http = require('http'); 8 | 9 | require('./common'); 10 | 11 | var server = http.createServer(function(req, res){ 12 | res.writeHead(200); 13 | res.end('Hello World'); 14 | }); 15 | 16 | cluster = cluster(server) 17 | .set('working directory', '/') 18 | .listen(3000); 19 | 20 | if (cluster.isWorker) { 21 | process.cwd().should.equal('/'); 22 | } 23 | 24 | cluster.on('listening', function(){ 25 | process.cwd().should.equal('/'); 26 | cluster.close(); 27 | }); --------------------------------------------------------------------------------