├── .gitignore ├── .hound.yml ├── .jshintrc ├── README.md ├── circle.yml ├── gulpfile.js ├── index.js ├── package.json ├── server └── app.js └── tests └── main.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | reports 10 | .env 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | .coverrun 24 | .DS_STORE 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 30 | node_modules 31 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | fail_on_violations: true 2 | 3 | javascript: 4 | config_file: .jshintrc 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": false, 3 | "bitwise": true, 4 | "browser": true, 5 | "camelcase": false, 6 | "curly": true, 7 | "forin": true, 8 | "immed": true, 9 | "latedef": "nofunc", 10 | "maxlen": 80, 11 | "newcap": true, 12 | "noarg": true, 13 | "noempty": true, 14 | "nonew": true, 15 | "esnext": true, 16 | "moz": true, 17 | "predef": [ 18 | "$", 19 | "jQuery", 20 | "jasmine", 21 | "beforeEach", 22 | "describe", 23 | "before", 24 | "it", 25 | "angular", 26 | "inject", 27 | "module", 28 | "require", 29 | "process", 30 | "__dirname", 31 | "console", 32 | "browser", 33 | "element", 34 | "by", 35 | "exports" 36 | ], 37 | "quotmark": true, 38 | "trailing": true, 39 | "undef": true, 40 | "unused": true 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Running Node JS (with Express) on Multiple Threads 2 | ================================================== 3 | 4 | [![Code Climate](https://codeclimate.com/github/andela-gukpere/multi-core-node-js-example/badges/gpa.svg)](https://codeclimate.com/github/andela-gukpere/multi-core-node-js-example) [![Test Coverage](https://codeclimate.com/github/andela-gukpere/multi-core-node-js-example/badges/coverage.svg)](https://codeclimate.com/github/andela-gukpere/multi-core-node-js-example/coverage) [![Circle CI](https://circleci.com/gh/gottsohn/multi-core-node-js-example.svg?style=svg)](https://circleci.com/gh/gottsohn/multi-core-node-js-example) [![Build Status](https://semaphoreci.com/api/v1/projects/6523fb33-acbd-4264-ba1e-daded0c2e048/603523/badge.svg)](https://semaphoreci.com/godson/multi-core-node-js-example) 5 | 6 | 7 | This is a simple example showing how to make NodeJS utilize your processor threads. 8 | 9 | The example makes use of an simple Express instance for HTTP serving. 10 | 11 | The express application can be found in `./server/app.js`, the NodeJS app useing [cluster](https://nodejs.org/api/cluster.html) to run the web server on multiple threads is the `index.js` script. 12 | 13 | Have a look at both scripts, the comments are sure to help. 14 | 15 | Here's a [blog post](http://blog.godson.com.ng/2015/11/running-node-js-with-express-multi-core-processors/) containing a stress test experiment with single and multi-threaded NodeJS instances. 16 | 17 | 18 | **NOTE** For Heroku Apps you'll want to use a ratio of the threads you are provided with else you run the risk of exceeding the Memory Limit designated to your application. See below a quick example of how you could manage this with Heroku. 19 | 20 | In line _8_ of _index.js_ you should have 21 | 22 | ```js 23 | let numCPUs = require('os').cpus().length / (parseInt(process.env.CLUSTER_DIVIDER, 10) || 1); 24 | ``` 25 | 26 | Where _CLUSTER\_DIVIDER_ is a config variable that determines the divider to be used. For Heroku most packages (Free and Standard 1X), you get 8 threads on a **Intel(R) Xeon(R) CPU E5-2670 v2 @ 2.50GHz**, so you'll want to use a divider of **2** (meaning you use only 4 threads) to avoid consuming all 512 MB of RAM. If your plan is up to 1 GB RAM then 8 threads will be fine. 27 | 28 | You might want to checkout `./tests/main.spec.js` and see how we test the multi-threading. 29 | 30 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 4.1.2 4 | dependencies: 5 | pre: 6 | post: 7 | - npm install module-deps 8 | - npm install gulp -g 9 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict'; 3 | const ENV = process.env.NODE_ENV || 'development'; 4 | if (ENV === 'development') { 5 | require('dotenv').load(); 6 | } 7 | 8 | let gulp = require('gulp'), 9 | jshint = require('gulp-jshint'), 10 | reporter = require('gulp-codeclimate-reporter'), 11 | istanbul = require('gulp-istanbul'), 12 | nodemon = require('gulp-nodemon'), 13 | mocha = require('gulp-mocha'), 14 | paths = { 15 | serverTests: ['./tests/*.spec.js'], 16 | app: ['./server/*.js'] 17 | }; 18 | 19 | gulp.task('pre-test', () => { 20 | return gulp.src(paths.app) 21 | .pipe(istanbul()) 22 | .pipe(istanbul.hookRequire()); 23 | }); 24 | 25 | gulp.task('test:server', ['pre-test'], () => { 26 | return gulp.src(paths.serverTests) 27 | .pipe(mocha({ 28 | reporter: 'spec' 29 | })) 30 | .once('error', err => { 31 | throw new Error(err); 32 | }) 33 | .pipe(istanbul({ 34 | includeUntested: true 35 | })) 36 | .pipe(istanbul.writeReports({ 37 | dir: './coverage', 38 | reporters: ['lcov'], 39 | reportOpts: { 40 | dir: './coverage' 41 | } 42 | })) 43 | .once('end', () => {}); 44 | }); 45 | 46 | gulp.task('lint', () => { 47 | return gulp.src(['./index.js', 'gulpfile.js', 48 | './server/**/*.js', './tests/**/*.js' 49 | ]) 50 | .pipe(jshint()) 51 | .pipe(jshint.reporter('default')); 52 | }); 53 | 54 | gulp.task('codeclimate-reporter', ['test:server'], () => { 55 | return gulp.src('coverage/lcov.info', { 56 | read: false 57 | }) 58 | .pipe(reporter({ 59 | token: process.env.CODECLIMATE_TOKEN, 60 | verbose: true 61 | })); 62 | }); 63 | 64 | gulp.task('nodemon', () => { 65 | nodemon({ 66 | script: 'index.js', 67 | ext: 'js', 68 | ignore: ['public/', 'node_modules/'] 69 | }) 70 | .on('change', ['lint']) 71 | .on('restart', () => { 72 | console.log('>> node restart'); 73 | }); 74 | }); 75 | 76 | gulp.task('test', ['codeclimate-reporter']); 77 | gulp.task('default', ['nodemon']); 78 | })(); 79 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict'; 3 | let cluster = require('cluster'); 4 | // Condition that checks if we are on the master process, 5 | // before creating child processes. 6 | if (cluster.isMaster) { 7 | // Fork all the workers. 8 | const numCPUs = require('os').cpus().length; 9 | // / (parseInt(process.env.CLUSTER_DIVIDER, 10) || 1); 10 | console.log('Process Master', process.pid); 11 | for (let i = 0; i < numCPUs; i++) { 12 | cluster.fork(); 13 | } 14 | 15 | Object.keys(cluster.workers).forEach(id => { 16 | console.log('Running with process ID: ', cluster.workers[id].process.pid); 17 | }); 18 | 19 | // arguments are worker, code, signal 20 | cluster.on('exit', worker => { 21 | const RESTART_DELAY = parseInt(process.env.RESTART_DELAY, 10) || 30000; 22 | console.log('Process ID: ' + worker.process.pid + 23 | ' died, creating new worker in ' + 24 | (RESTART_DELAY / 1000) + ' seconds'); 25 | setTimeout(cluster.fork, RESTART_DELAY); 26 | }); 27 | } else { 28 | const PORT = process.env.PORT || 5555; 29 | require('./server/app')(process.cwd(), app => { 30 | app.listen(PORT, err => { 31 | console.log(err || 'Server running on ', PORT, 32 | ' Process ID: ' + process.pid); 33 | }); 34 | }); 35 | } 36 | })(); 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multi-core-node-js-example", 3 | "version": "1.0.0", 4 | "description": "Example using node-js cluster and express", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node index.js &\ngulp test" 8 | }, 9 | "engines": { 10 | "node": "5.1.0", 11 | "npm": "3.3.12" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/andela-gukpere/multi-core-node-js-example.git" 16 | }, 17 | "keywords": [ 18 | "cluster", 19 | "nodejs", 20 | "es6", 21 | "multicore", 22 | "thread" 23 | ], 24 | "author": "Godson Ukpere", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/andela-gukpere/multi-core-node-js-example/issues" 28 | }, 29 | "homepage": "https://github.com/andela-gukpere/multi-core-node-js-example#readme", 30 | "dependencies": { 31 | "body-parser": "^1.14.1", 32 | "dotenv": "^1.2.0", 33 | "expect.js": "^0.3.1", 34 | "express": "^4.13.3", 35 | "gulp": "^3.9.0", 36 | "gulp-codeclimate-reporter": "^1.1.1", 37 | "gulp-istanbul": "^0.10.2", 38 | "gulp-jshint": "^1.12.0", 39 | "gulp-mocha": "^2.1.3", 40 | "gulp-nodemon": "^2.0.4", 41 | "jshint": "^2.8.0", 42 | "moment": "^2.10.6", 43 | "superagent": "^1.4.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict'; 3 | const ENV = process.env.NODE_ENV || 'development'; 4 | let express = require('express'), 5 | cluster = require('cluster'), 6 | moment = require('moment'), 7 | app = express(), 8 | bodyParser = require('body-parser'); 9 | 10 | module.exports = (appdir, cb) => { 11 | app.dir = appdir; 12 | 13 | // static files 14 | app.use(express.static(app.dir + '/public')); 15 | 16 | // things to do on each request 17 | app.use((req, res, next) => { 18 | // log each request in development/staging ENVironment 19 | if (ENV !== 'production') { 20 | console.log(moment().format('HH:MM'), req.method, req.url, 21 | req.socket.bytesRead, 'process:', process.pid); 22 | } 23 | next(); 24 | }); 25 | 26 | // Standard error handling 27 | app.use((err, req, res, next) => { 28 | console.error(err.stack); 29 | res.status(500).send('Something broke!'); 30 | next(); 31 | }); 32 | 33 | // to support JSON-encoded bodies 34 | app.use(bodyParser.json()); 35 | 36 | // to support URL-encoded bodies 37 | app.use(bodyParser.urlencoded({ 38 | extended: true 39 | })); 40 | 41 | app.get('/cluster', (req, res) => 42 | res.send({ 43 | isMaster: cluster.isMaster, 44 | isWorker: cluster.isWorker, 45 | workers: cluster.workers, 46 | pid: process.pid 47 | }) 48 | ); 49 | // Dummy route to return JSON 50 | app.get('/', (req, res) => { 51 | res.send(require('http').STATUS_CODES); 52 | }); 53 | 54 | // callback from /index.js 55 | cb(app); 56 | }; 57 | })(); 58 | -------------------------------------------------------------------------------- /tests/main.spec.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict'; 3 | const APP_URL = 'http://localhost:' + (process.env.PORT || 5555) + '/', 4 | THREAD_COUNT = require('os').cpus().length; 5 | 6 | let request = require('superagent'), 7 | expect = require('expect.js'); 8 | 9 | describe('Node JS Multi-Threaded Instance', () => { 10 | describe('Instance', () => { 11 | it('should be a worker, and not the master', done => { 12 | request.get(APP_URL + 'cluster'). 13 | accept('application/json') 14 | .end((err, res) => { 15 | expect(err).to.be(null); 16 | expect(res.status).to.be(200); 17 | expect(res.body).to.be.a('object'); 18 | expect(res.body.isMaster).to.be(false); 19 | expect(res.body.isMaster).to.be.a('boolean'); 20 | expect(res.body.isWorker).to.be(true); 21 | expect(res.body.pid).to.be.a('number'); 22 | done(); 23 | }); 24 | }); 25 | 26 | it('should load balance on threads using various processes', done => { 27 | let count = 0, 28 | requestCount = 200, 29 | pids = [], 30 | cb = body => { 31 | count++; 32 | if (pids.indexOf(body.pid) === -1) { 33 | pids.push(body.pid); 34 | } 35 | 36 | if (count === requestCount) { 37 | console.log('Process IDs', pids); 38 | expect(pids).to.not.be.empty(); 39 | expect(pids.length).to.be.greaterThan(1); 40 | if (requestCount > THREAD_COUNT) { 41 | // All threads will be used if the number of 42 | // requests are equal or more then the threads 43 | expect(pids.length).to.be(THREAD_COUNT); 44 | } else { 45 | // If the requests are less than the number of 46 | // threads, threads are used for each request only 47 | expect(pids.length).to.be(requestCount); 48 | } 49 | done(); 50 | } 51 | }; 52 | 53 | console.log('You have', THREAD_COUNT, 'CPU thread(s)'); 54 | // Make simultaneous requests to the server to force load balacing. 55 | for (let i = 0; i < requestCount; i++) { 56 | console.log('Making request number', i + 1); 57 | request.get(APP_URL + 'cluster'). 58 | accept('application/json') 59 | .end((err, res) => { 60 | expect(err).to.be(null); 61 | expect(res.status).to.be(200); 62 | cb(res.body); 63 | }); 64 | } 65 | }); 66 | }); 67 | }); 68 | })(); 69 | --------------------------------------------------------------------------------