├── .gitignore ├── stress ├── worker.js └── stress.js ├── test ├── workers │ ├── echo.js │ ├── getenv.js │ └── sleep.js ├── basic-test.js ├── child-env-test.js ├── inside-nodecluster-test.js ├── maximum-backlog-test.js ├── child-death-test.js ├── maximum-duration-test.js └── information-output-test.js ├── .travis.yml ├── example ├── simple_worker.js ├── after_worker.js ├── simple.js ├── before.js └── after.js ├── ChangeLog ├── LICENSE ├── package.json ├── README.md └── lib └── compute-cluster.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *~ 3 | -------------------------------------------------------------------------------- /stress/worker.js: -------------------------------------------------------------------------------- 1 | process.on('message', function(m) { process.send(m); }); 2 | -------------------------------------------------------------------------------- /test/workers/echo.js: -------------------------------------------------------------------------------- 1 | process.on('message', function(m) { 2 | process.send(m); 3 | }); 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.6 5 | 6 | notifications: 7 | email: 8 | - lloyd@hilaiel.com 9 | -------------------------------------------------------------------------------- /test/workers/getenv.js: -------------------------------------------------------------------------------- 1 | process.on('message', function(m) { 2 | process.send({ key: m, value: process.env[m] }); 3 | }); 4 | -------------------------------------------------------------------------------- /test/workers/sleep.js: -------------------------------------------------------------------------------- 1 | process.on('message', function(m) { 2 | setTimeout(function() { 3 | process.send(m); 4 | }, m); 5 | }); 6 | -------------------------------------------------------------------------------- /example/simple_worker.js: -------------------------------------------------------------------------------- 1 | process.on('message', function(m) { 2 | for (var i = 0; i < 100000000; i++); 3 | process.send('complete'); 4 | }); 5 | -------------------------------------------------------------------------------- /example/after_worker.js: -------------------------------------------------------------------------------- 1 | process.on('message', function(args) { 2 | // do work 3 | for (var i = 0; i < 100000000; i++); 4 | process.send('complete'); 5 | }); 6 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 0.0.5: 2 | * fix bug - queued work should be started even if enqueue is invoked from a completion callback. 3 | 4 | 0.0.4: 5 | * explicitly relay env to children, as default of child_process.fork seems to be not to do this 6 | 7 | 0.0.3: 8 | * fix github url in package.json 9 | 10 | 0.0.2: 11 | * the callback to .exit() really is optional 12 | 13 | 0.0.1: 14 | * the beginning of time 15 | -------------------------------------------------------------------------------- /example/simple.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const computecluster = require('../lib/compute-cluster'); 4 | 5 | // allocate a compute cluster 6 | var cc = new computecluster({ 7 | module: './simple_worker.js' 8 | }); 9 | 10 | var toRun = 10 11 | 12 | // then you can perform work in parallel 13 | for (var i = 0; i < toRun; i++) { 14 | cc.enqueue({}, function(err, r) { 15 | if (err) console.log("an error occured:", err); 16 | else console.log("it's nice:", r); 17 | if (--toRun === 0) cc.exit(); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /example/before.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | function doWork() { 4 | for (var i = 0; i < 100000000; i++); 5 | } 6 | 7 | var workDone = 0; 8 | 9 | var starttime = new Date(); 10 | var lastoutput = starttime; 11 | 12 | while (true) { 13 | doWork(); 14 | workDone++; 15 | if (lastoutput.getTime() + (3 * 1000) < (new Date()).getTime()) 16 | { 17 | lastoutput = new Date(); 18 | console.log((workDone / ((lastoutput - starttime) / 1000.0)).toFixed(2), 19 | "units work performed per second"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Lloyd Hilaiel 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Lloyd Hilaiel (http://lloyd.io)", 3 | "name": "compute-cluster", 4 | "description": "Local process cluster management for distributed computation", 5 | "version": "0.0.9", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/lloyd/node-compute-cluster.git" 9 | }, 10 | "main": "lib/compute-cluster", 11 | "engines": { 12 | "node": ">= 0.6.2" 13 | }, 14 | "dependencies": { 15 | "vows": "0.6.0" 16 | }, 17 | "devDependencies": {}, 18 | "scripts": { 19 | "test": "vows" 20 | }, 21 | "bugs": "https://github.com/lloyd/node-compute-cluster/issues", 22 | "licenses": { 23 | "type": "MIT", 24 | "url": "https://raw.github.com/lloyd/node-compute-cluster/master/LICENSE" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /stress/stress.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const 4 | computecluster = require('../lib/compute-cluster'), 5 | os = require('os'), 6 | crypto = require('crypto'); 7 | 8 | // allocate a compute cluster 9 | var cc = new computecluster({ 10 | module: './worker.js', 11 | max_processes: os.cpus().length * 10 // 10x more procs than cpus 12 | }); 13 | 14 | function addWork() { 15 | crypto.randomBytes(16, function(ex, buf) { 16 | if (ex) throw ex; 17 | var str = buf.toString('base64'); 18 | cc.enqueue(str, function(err, r) { 19 | if (err) { 20 | process.stderr.write("to err is lame! err: " + err + "\n"); 21 | process.exit(9); 22 | } 23 | if (r !== str) { 24 | process.stderr.write("string not problerly echo'd. LAME!\n"); 25 | process.stderr.write("want/got: " + str + "/" + r + "\n"); 26 | process.exit(9); 27 | } 28 | console.log(str); 29 | addWork(); 30 | }); 31 | }); 32 | } 33 | 34 | // then you can perform work in parallel 35 | for (var i = 0; i < os.cpus().length * 39; i++) addWork(); 36 | -------------------------------------------------------------------------------- /example/after.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const 4 | os = require('os'), 5 | path = require('path'), 6 | ComputeCluster = require('../lib/compute-cluster.js'); 7 | 8 | // allocate a compute cluster 9 | var computeCluster = new ComputeCluster({ 10 | module: path.join(__dirname, "after_worker.js") 11 | }); 12 | 13 | // if you don't handle errors, they will take down the process 14 | computeCluster.on('error', function(e) { 15 | process.stderr.write('unexpected error from compute cluster: ' + e + "\n"); 16 | process.exit(1); 17 | }); 18 | 19 | var workDone = 0; 20 | var starttime = new Date(); 21 | var lastoutput = starttime; 22 | 23 | var outstanding = 0; 24 | const MAX_OUTSTANDING = os.cpus().length * 2; 25 | 26 | function addWork() { 27 | while (outstanding < MAX_OUTSTANDING) { 28 | outstanding++; 29 | computeCluster.enqueue({}, function(err, r) { 30 | outstanding--; 31 | workDone++; 32 | 33 | if (lastoutput.getTime() + (3 * 1000) < (new Date()).getTime()) 34 | { 35 | lastoutput = new Date(); 36 | console.log((workDone / ((lastoutput - starttime) / 1000.0)).toFixed(2), 37 | "units work performed per second"); 38 | } 39 | 40 | addWork(); 41 | }); 42 | } 43 | } 44 | 45 | addWork(); 46 | -------------------------------------------------------------------------------- /test/basic-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const 4 | vows = require('vows'), 5 | assert = require('assert'), 6 | computeCluster = require('../lib/compute-cluster'), 7 | path = require('path'); 8 | 9 | var suite = vows.describe('basic tests'); 10 | 11 | // disable vows (often flakey?) async error behavior 12 | suite.options.error = false; 13 | 14 | suite.addBatch({ 15 | "allocation of a compute cluster": { 16 | topic: function() { 17 | return new computeCluster({ 18 | module: path.join(__dirname, 'workers', 'echo.js') 19 | }); 20 | }, 21 | "runs without issue": function (cc) { 22 | assert.isObject(cc); 23 | }, 24 | "and invocation against this cluster": { 25 | topic: function(cc) { 26 | var cb = this.callback; 27 | cc.enqueue("hello", function(e, r) { 28 | cb.call(self, { cc: cc, r: r }); 29 | }); 30 | 31 | }, 32 | "succeeds": function (r) { 33 | assert.equal(r.r, 'hello'); 34 | }, 35 | "finally, exit": { 36 | topic: function(r) { 37 | r.cc.exit(this.callback); 38 | }, 39 | "also succeeds": function(err) { 40 | assert.isNull(err); 41 | } 42 | } 43 | } 44 | } 45 | }); 46 | 47 | // run or export the suite. 48 | if (process.argv[1] === __filename) suite.run(); 49 | else suite.export(module); 50 | -------------------------------------------------------------------------------- /test/child-env-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const 4 | vows = require('vows'), 5 | assert = require('assert'), 6 | computeCluster = require('../lib/compute-cluster'), 7 | path = require('path'); 8 | 9 | var suite = vows.describe('basic tests'); 10 | 11 | // disable vows (often flakey?) async error behavior 12 | suite.options.error = false; 13 | 14 | suite.addBatch({ 15 | "allocation of a compute cluster": { 16 | topic: function() { 17 | return new computeCluster({ 18 | module: path.join(__dirname, 'workers', 'getenv.js') 19 | }); 20 | }, 21 | "runs without issue": function (cc) { 22 | assert.isObject(cc); 23 | }, 24 | "and invocation against this cluster": { 25 | topic: function(cc) { 26 | process.env['FOO'] = 'bar'; 27 | var cb = this.callback; 28 | cc.enqueue("FOO", function(e, r) { 29 | cb.call(self, { cc: cc, r: r }); 30 | }); 31 | }, 32 | "succeeds": function (r) { 33 | assert.equal(r.r.key, 'FOO'); 34 | assert.equal(r.r.value, 'bar'); 35 | }, 36 | "finally, exit": { 37 | topic: function(r) { 38 | r.cc.exit(this.callback); 39 | }, 40 | "also succeeds": function(err) { 41 | assert.isNull(err); 42 | } 43 | } 44 | } 45 | } 46 | }); 47 | 48 | // run or export the suite. 49 | if (process.argv[1] === __filename) suite.run(); 50 | else suite.export(module); 51 | -------------------------------------------------------------------------------- /test/inside-nodecluster-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const 4 | vows = require('vows'), 5 | assert = require('assert'), 6 | nodeCluster = require('cluster'), 7 | computeCluster = require('../lib/compute-cluster'), 8 | path = require('path'); 9 | 10 | if(nodeCluster.isMaster) { 11 | nodeCluster.fork(); 12 | return; 13 | } 14 | 15 | var suite = vows.describe('basic tests'); 16 | 17 | // disable vows (often flakey?) async error behavior 18 | suite.options.error = false; 19 | 20 | suite.addBatch({ 21 | "allocation of a compute cluster": { 22 | topic: function() { 23 | return new computeCluster({ 24 | module: path.join(__dirname, 'workers', 'echo.js') 25 | }); 26 | }, 27 | "runs without issue": function (cc) { 28 | assert.isObject(cc); 29 | }, 30 | "and invocation against this cluster": { 31 | topic: function(cc) { 32 | var cb = this.callback; 33 | cc.enqueue("hello", function(e, r) { 34 | cb.call(self, { cc: cc, r: r }); 35 | }); 36 | 37 | }, 38 | "succeeds": function (r) { 39 | assert.equal(r.r, 'hello'); 40 | }, 41 | "finally, exit": { 42 | topic: function(r) { 43 | r.cc.exit(this.callback); 44 | }, 45 | "also succeeds": function(err) { 46 | assert.isNull(err); 47 | } 48 | } 49 | } 50 | } 51 | }); 52 | 53 | // run or export the suite. 54 | if (process.argv[1] === __filename) suite.run(); 55 | else suite.export(module); 56 | -------------------------------------------------------------------------------- /test/maximum-backlog-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const 4 | vows = require('vows'), 5 | assert = require('assert'), 6 | computeCluster = require('../lib/compute-cluster'), 7 | path = require('path'); 8 | 9 | var suite = vows.describe('basic tests'); 10 | 11 | // disable vows (often flakey?) async error behavior 12 | suite.options.error = false; 13 | 14 | suite.addBatch({ 15 | "allocation of a compute cluster": { 16 | topic: function() { 17 | return new computeCluster({ 18 | module: path.join(__dirname, 'workers', 'echo.js'), 19 | max_processes: 1, 20 | max_backlog: 2 21 | }); 22 | }, 23 | "runs without issue": function (cc) { 24 | assert.isObject(cc); 25 | }, 26 | "enqueing too much work": { 27 | topic: function(cc) { 28 | var cb = this.callback; 29 | for (var i = 0; i < 4; i++) { 30 | cc.enqueue(i, function (err, r) { 31 | if (err) cb({ err: err, cc: cc }); 32 | }); 33 | } 34 | }, 35 | "succeeds": function (r) { 36 | assert.equal(r.err, 'cannot enqueue work: maximum backlog exceeded (2)'); 37 | }, 38 | "finally, exit": { 39 | topic: function(r) { 40 | r.cc.exit(this.callback); 41 | }, 42 | "also succeeds": function(err) { 43 | assert.isNull(err); 44 | } 45 | } 46 | } 47 | } 48 | }); 49 | 50 | // run or export the suite. 51 | if (process.argv[1] === __filename) suite.run(); 52 | else suite.export(module); 53 | -------------------------------------------------------------------------------- /test/child-death-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const 4 | vows = require('vows'), 5 | assert = require('assert'), 6 | computeCluster = require('../lib/compute-cluster'), 7 | path = require('path'); 8 | 9 | var suite = vows.describe('basic tests'); 10 | 11 | // disable vows (often flakey?) async error behavior 12 | suite.options.error = false; 13 | 14 | suite.addBatch({ 15 | "allocation of a compute cluster": { 16 | topic: function() { 17 | return new computeCluster({ 18 | module: path.join(__dirname, 'workers', 'echo.js') 19 | }); 20 | }, 21 | "runs without issue": function (cc) { 22 | assert.isObject(cc); 23 | }, 24 | "and invocation against this cluster": { 25 | topic: function(cc) { 26 | var cb = this.callback; 27 | cc.enqueue("hello", function(e, r) { 28 | cb.call(self, { cc: cc, r: r }); 29 | }); 30 | 31 | }, 32 | "succeeds": function (r) { 33 | assert.equal(r.r, 'hello'); 34 | }, 35 | "and killing a child": { 36 | topic: function(r) { 37 | var cb = this.callback; 38 | r.cc.on('error', function(e) { 39 | cb(e); 40 | }); 41 | process.kill(Object.keys(r.cc._kids)[0]); 42 | }, 43 | "causes an error event": function(err) { 44 | assert.isString(err); 45 | assert.match(err, /^compute process \(\d+\) dies with code: 1$/); 46 | } 47 | } 48 | } 49 | } 50 | }); 51 | 52 | // run or export the suite. 53 | if (process.argv[1] === __filename) suite.run(); 54 | else suite.export(module); 55 | -------------------------------------------------------------------------------- /test/maximum-duration-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const 4 | vows = require('vows'), 5 | assert = require('assert'), 6 | computeCluster = require('../lib/compute-cluster'), 7 | path = require('path'); 8 | 9 | var suite = vows.describe('basic tests'); 10 | 11 | // disable vows (often flakey?) async error behavior 12 | suite.options.error = false; 13 | 14 | suite.addBatch({ 15 | "allocation of a compute cluster": { 16 | topic: function() { 17 | return new computeCluster({ 18 | module: path.join(__dirname, 'workers', 'sleep.js'), 19 | max_processes: 2, 20 | max_backlog: -1, // no maximum backlog 21 | max_request_time: 0.5 // 500ms maximum allowed time 22 | }); 23 | }, 24 | "runs without issue": function (cc) { 25 | assert.isObject(cc); 26 | }, 27 | "enqueing too much work": { 28 | topic: function(cc) { 29 | // run a bunch of 50ms sleeps 30 | var cb = this.callback; 31 | for (var i = 0; i < 50; i++) { 32 | cc.enqueue(50, function (err, r) { 33 | if (err) cb({ err: err, cc: cc }); 34 | cc.enqueue(50, function (err, r) { 35 | if (err) cb({ err: err, cc: cc }); 36 | }); 37 | }); 38 | } 39 | }, 40 | "fails": function (r) { 41 | assert.ok(/cannot enqueue work: maximum expected work duration exceeded \(\d.\d+s\)/.test(r.err)); 42 | }, 43 | "finally, exit": { 44 | topic: function(r) { 45 | r.cc.exit(this.callback); 46 | }, 47 | "also succeeds": function(err) { 48 | assert.isNull(err); 49 | } 50 | } 51 | } 52 | } 53 | }); 54 | 55 | // run or export the suite. 56 | if (process.argv[1] === __filename) suite.run(); 57 | else suite.export(module); 58 | -------------------------------------------------------------------------------- /test/information-output-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const 4 | vows = require('vows'), 5 | assert = require('assert'), 6 | computeCluster = require('../lib/compute-cluster'), 7 | path = require('path'); 8 | 9 | var suite = vows.describe('basic tests'); 10 | 11 | // disable vows (often flakey?) async error behavior 12 | suite.options.error = false; 13 | 14 | var messages = [ 15 | ]; 16 | 17 | suite.addBatch({ 18 | "allocation of a compute cluster": { 19 | topic: function() { 20 | return new computeCluster({ 21 | module: path.join(__dirname, 'workers', 'echo.js') 22 | }).on('info', function(m) { 23 | messages.push({ type: 'info', msg: m }); 24 | }).on('debug', function(m) { 25 | messages.push({ type: 'debug', msg: m }); 26 | }); 27 | }, 28 | "runs without issue": function (cc) { 29 | assert.isObject(cc); 30 | }, 31 | "and invocation against this cluster": { 32 | topic: function(cc) { 33 | var cb = this.callback; 34 | cc.enqueue("hello", function(e, r) { 35 | cb.call(self, { cc: cc, r: r }); 36 | }); 37 | 38 | }, 39 | "succeeds": function (r) { 40 | assert.equal(r.r, 'hello'); 41 | }, 42 | "finally, exit": { 43 | topic: function(r) { 44 | r.cc.exit(this.callback); 45 | }, 46 | "also succeeds": function(err) { 47 | assert.isNull(err); 48 | }, 49 | "and once complete": { 50 | topic: messages, 51 | "we have expected informational messages": function (m) { 52 | // verify that we've got some messages 53 | assert.isTrue(m.length > 0); 54 | // verify we've got some info and some debug msgs 55 | var numInfo = 0, numDebug = 0, numUnknown = 0; 56 | m.forEach(function(m) { 57 | if (m.type === 'info') numInfo++; 58 | else if (m.type === 'debug') numDebug++; 59 | else numUnknown++; 60 | }); 61 | assert.isTrue(numInfo > 0); 62 | assert.isTrue(numDebug > 0); 63 | assert.strictEqual(numUnknown, 0); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | }); 70 | 71 | // run or export the suite. 72 | if (process.argv[1] === __filename) suite.run(); 73 | else suite.export(module); 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Distributed Computation for NodeJS 2 | 3 | [![Build Status](https://secure.travis-ci.org/lloyd/node-compute-cluster.png)](http://travis-ci.org/lloyd/node-compute-cluster) 4 | 5 | How can you build a responsive and robust nodejs server that does some heavy 6 | computational lifting? Some node libraries (like the awesome [node-bcrypt][]) 7 | do their own threading internally and combine that with an async API. This 8 | allows libraries to internally thread their calls and use multiple cores. 9 | 10 | [node-bcrypt]: https://github.com/ncb000gt/node.bcrypt.js 11 | 12 | While this is pretty awesome, it is significant work for library implementors, 13 | and as this pattern becomes rampant, the application author loses fine grained 14 | control over the resource usage of their server as well as the relative priority 15 | of compute tasks. 16 | 17 | If you just naively run computation on the main evaluation thread, you're blocking 18 | node.js from doing *anything else* and making your whole server unresponsive. 19 | 20 | ## The solution? 21 | 22 | `node-compute-cluster` is a tiny abstraction around a group of 23 | processes and the [built-in IPC][] introduced in NodeJS 0.6.x. It provides a simple 24 | API by which you can allocate and run work on a cluster of computation processes. 25 | This allows you to perform multiprocessing at a more granular level, and produce 26 | a responsive yet efficient computation server. 27 | 28 | [built-in IPC]: http://nodejs.org/docs/v0.6.3/api/all.html#child_process.fork 29 | 30 | ## Installation 31 | 32 | ``` sh 33 | $ npm install compute-cluster 34 | ``` 35 | 36 | ## Usage 37 | 38 | First you write your main program: 39 | 40 | ``` js 41 | const computecluster = require('compute-cluster'); 42 | 43 | // allocate a compute cluster 44 | var cc = new computecluster({ 45 | module: './worker.js' 46 | }); 47 | 48 | var toRun = 10 49 | 50 | // then you can perform work in parallel 51 | for (var i = 0; i < toRun; i++) { 52 | cc.enqueue({}, function(err, r) { 53 | if (err) console.log("an error occured:", err); 54 | else console.log("it's nice:", r); 55 | if (--toRun === 0) cc.exit(); 56 | }); 57 | }; 58 | ``` 59 | 60 | Next you write your `worker.js` program: 61 | 62 | ``` js 63 | process.on('message', function(m) { 64 | for (var i = 0; i < 100000000; i++); 65 | process.send('complete'); 66 | }); 67 | ``` 68 | 69 | All done! Now you're distributing your computational load across multiple processes. 70 | 71 | ## API 72 | 73 | ### Constructor - `new require('compute-cluster')();` 74 | 75 | Allocates a computation cluster. Options include: 76 | 77 | * `module` - **required** the path to the module to load 78 | * `max_processes` - the maximum number of processes to spawn (default is `ciel(#cpus * 1.25)`) 79 | * `max_backlog` - the maximum length of the backlog, -1 indicates no limit (default is 10 * max_processes) 80 | an error will be returned when max backlog is hit. 81 | * `max_request_time` - the maximum amount of time a request should take, in seconds. An error will be returned when we expect a request will take longer. 82 | 83 | Example: 84 | 85 | ``` js 86 | var cc = new require('compute-cluster')({ 87 | module: './foo.js', 88 | max_backlog: -1 89 | }); 90 | ``` 91 | 92 | ### Event: 'error' 93 | 94 | An error event will be emited in exceptional circumstances. Like if a child crashes. 95 | Catch error events like this: 96 | 97 | ``` js 98 | cc.on('error', function(e) { console.log('OMG!', e); }); 99 | ``` 100 | 101 | Default behavior is to exit on error if you don't catch. 102 | 103 | ### Events: 'debug' or 'info' 104 | 105 | Events raise that hold an english, developer readable string describing 106 | the state of the implementation. 107 | 108 | ### cc.enqueue(, [cb]) 109 | 110 | enqueue a job to be run on the next available compute process, spawning one 111 | if required (and `max_processes` isn't hit). 112 | 113 | args will be passed into the process (available via `process.on('message', ...)`). 114 | 115 | `cb` is optional, and will be invoked with two params, `err` and `response`. 116 | `err` indicates hard errors, response indicates successful roundtrip to the 117 | compute process and is whatever the decided to `process.send()` in response. 118 | 119 | ### cc.exit([cb]) 120 | 121 | Kill all child processes, invoking callback (with err param) when complete. 122 | 123 | ## LICENSE 124 | 125 | Copyright (c) 2011, Lloyd Hilaiel 126 | 127 | Permission to use, copy, modify, and/or distribute this software for any 128 | purpose with or without fee is hereby granted, provided that the above 129 | copyright notice and this permission notice appear in all copies. 130 | 131 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 132 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 133 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 134 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 135 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 136 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 137 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 138 | -------------------------------------------------------------------------------- /lib/compute-cluster.js: -------------------------------------------------------------------------------- 1 | const 2 | util = require('util'), 3 | existsSync = require('fs').existsSync || require('path').existsSync, 4 | child_process = require('child_process'), 5 | events = require('events'); 6 | 7 | // decaying factor for heurstics calculating how much work costs. 8 | const MAX_HISTORY = 100; 9 | 10 | function ComputeCluster(options) { 11 | if (!options || typeof options.module !== 'string') { 12 | throw "missing required 'module' argument"; 13 | } 14 | // is module a file? 15 | if (!existsSync(options.module)) { 16 | throw "module doesn't exist: " + options.module; 17 | } 18 | if (options.max_processes && 19 | (typeof options.max_processes !== 'number' || options.max_processes < 1)) { 20 | throw "when provided, max_processes must be an integer greater than one"; 21 | } 22 | if (options.max_backlog && typeof options.max_backlog != 'number') { 23 | throw "when provided, max_backlog must be a number"; 24 | } 25 | if (options.max_request_time && typeof options.max_request_time != 'number') { 26 | throw "when provided, max_request_time must be a number"; 27 | } 28 | 29 | events.EventEmitter.call(this); 30 | 31 | // an array of child processes 32 | this._kids = {}; 33 | this._MAX_KIDS = (options.max_processes || Math.ceil(require('os').cpus().length * 1.25)); 34 | this._work_q = []; 35 | this._exiting = false; 36 | this._exit_cb; 37 | this._module = options.module; 38 | // how long shall we allow our queue to get before we stop accepting work? 39 | // (negative implies no limit. careful, there.) 40 | this._MAX_BACKLOG = options.max_backlog || this._MAX_KIDS * 10; 41 | this._MAX_REQUEST_TIME = options.max_request_time || 0; 42 | this._work_duration = 0; 43 | this._jobs_run = 0; 44 | }; 45 | 46 | util.inherits(ComputeCluster, events.EventEmitter); 47 | 48 | ComputeCluster.prototype._onWorkerExit = function(pid) { 49 | var self = this; 50 | return function (code) { 51 | // inform the callback the process quit 52 | var worker = self._kids[pid]; 53 | if (worker && worker.job && worker.job.cb) { 54 | worker.job.cb("compute process (" + pid + ") dies with code: " + code); 55 | } 56 | // if _exiting is false, we don't expect to be shutting down! 57 | if (!self._exiting) { 58 | self.emit('error', 59 | "compute process (" + pid + ") dies with code: " + code); 60 | } 61 | 62 | self.emit('info', "compute process (" + pid + ") exits with code " + code); 63 | 64 | delete self._kids[pid]; 65 | if (self._exit_cb) { 66 | if (Object.keys(self._kids).length === 0) { 67 | self._exit_cb(null); 68 | _exit_cb = undefined; 69 | } 70 | } 71 | } 72 | }; 73 | 74 | ComputeCluster.prototype._getEnvForWorker = function() { 75 | var env = {}; 76 | for (var i in process.env) { 77 | env[i] = process.env[i]; 78 | } 79 | 80 | delete env.NODE_WORKER_ID; //Node.js cluster worker marker for v0.6 81 | delete env.NODE_UNIQUE_ID; //Node.js cluster worker marker for v0.7 82 | 83 | return env; 84 | }; 85 | 86 | ComputeCluster.prototype._getFreeWorker = function() { 87 | var self = this; 88 | 89 | for (var i in this._kids) { 90 | if (!this._kids[i].job) return this._kids[i]; 91 | } 92 | 93 | // no workers! can we spawn one? 94 | if (Object.keys(this._kids).length < this._MAX_KIDS) { 95 | var k = { 96 | worker: child_process.fork( 97 | this._module, 98 | [], 99 | { env: this._getEnvForWorker() } 100 | ) }; 101 | k.worker.on('exit', this._onWorkerExit(k.worker.pid)); 102 | this._kids[k.worker.pid] = k; 103 | 104 | this.emit('info', "spawned new worker process (" + k.worker.pid + ") " + 105 | Object.keys(this._kids).length + "/" + this._MAX_KIDS + " processes running"); 106 | 107 | return k; 108 | } 109 | }; 110 | 111 | ComputeCluster.prototype._runWorkOnWorker = function(work, worker) { 112 | var self = this; 113 | this.emit("debug", "passing compute job to process " + worker.worker.pid); 114 | var startTime = new Date(); 115 | worker.worker.once('message', function(m) { 116 | // clear the in-progress job 117 | var cb = worker.job.cb; 118 | delete worker.job; 119 | 120 | // start the next 121 | self._assignWork(); 122 | 123 | // call our client's callback 124 | if (cb) cb(null, m); 125 | // emit some debug info 126 | var timeMS = (new Date() - startTime); 127 | self.emit("debug", "process " + worker.worker.pid + " completed work in " + 128 | (timeMS / 1000.0).toFixed(2) + "s"); 129 | 130 | // if there is a maximum request time, perform some math to estimate how 131 | // long requests take, favoring history to current job in proportion: 132 | // MAX_HISTORY:1 133 | if (self._MAX_REQUEST_TIME && self._jobs_run >= (2 * self._MAX_KIDS)) { 134 | var history = (self._jobs_run > MAX_HISTORY) ? MAX_HISTORY : self._jobs_run; 135 | self._work_duration = ((self._work_duration * history) + timeMS) / (history + 1); 136 | } 137 | 138 | self._jobs_run++; 139 | }); 140 | worker.worker.send(work.job); 141 | worker.job = work; 142 | }; 143 | 144 | // assign as many work units from work_q as possible to avialable 145 | // compute processes 146 | ComputeCluster.prototype._assignWork = function() { 147 | while (this._work_q.length > 0) { 148 | var worker = this._getFreeWorker(); 149 | if (!worker) break; 150 | 151 | this._runWorkOnWorker(this._work_q.shift(), worker); 152 | } 153 | }; 154 | 155 | ComputeCluster.prototype.enqueue = function(args, cb) { 156 | // maximum allowed request time check 157 | if (this._MAX_REQUEST_TIME && this._jobs_run > (2 * this._MAX_KIDS)) { 158 | var numWorkers = Object.keys(this._kids).length * 1.0; 159 | if (numWorkers) { 160 | // how long would this work take? 161 | var expected = ((this._work_q.length / numWorkers) * this._work_duration + this._work_duration) / 1000.0; 162 | if (expected > this._MAX_REQUEST_TIME) { 163 | this.emit('info', "maximum expected work duration hit hit (work would take about " + expected + 164 | "s, which is greater than "+ this._MAX_REQUEST_TIME +")! cannot enqueue!"); 165 | process.nextTick(function() { 166 | cb("cannot enqueue work: maximum expected work duration exceeded (" + expected + "s)"); 167 | }); 168 | return this; 169 | } 170 | } 171 | } 172 | 173 | // backlog size check 174 | if (this._MAX_BACKLOG > 0 && this._work_q.length >= this._MAX_BACKLOG) { 175 | this.emit('info', "maximum work backlog hit (" + this._MAX_BACKLOG + 176 | ")! cannot enqueue additional work!"); 177 | var mb = this._MAX_BACKLOG; 178 | process.nextTick(function() { 179 | cb("cannot enqueue work: maximum backlog exceeded (" + mb + ")"); 180 | }); 181 | return this; 182 | } 183 | 184 | this._work_q.push({ job: args, cb: cb }); 185 | this._assignWork(); 186 | return this; 187 | }; 188 | 189 | ComputeCluster.prototype.exit = function(cb) { 190 | if (Object.keys(this._kids).length === 0) { 191 | if (cb) setTimeout(function() { cb(null); }, 0); 192 | } else { 193 | this._exiting = true; 194 | this._exit_cb = cb; 195 | this.emit('info', "exit called, shutting down " + Object.keys(this._kids).length + 196 | " child processes"); 197 | for (var i in this._kids) { 198 | this._kids[i].worker.kill(); 199 | } 200 | } 201 | return this; 202 | }; 203 | 204 | module.exports = ComputeCluster; 205 | --------------------------------------------------------------------------------