├── .editorconfig ├── .gitignore ├── .jshintignore ├── .jshintrc ├── changelog.markdown ├── license ├── lipstick.js ├── package.json └── readme.markdown /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | Thumbs.db 5 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | dist 4 | example 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "newcap": true, 5 | "noarg": true, 6 | "noempty": true, 7 | "nonew": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "trailing": true, 12 | "boss": true, 13 | "eqnull": true, 14 | "strict": true, 15 | "immed": true, 16 | "expr": true, 17 | "latedef": "nofunc", 18 | "quotmark": "single", 19 | "validthis": true, 20 | "indent": 2, 21 | "node": true, 22 | "browser": true 23 | } 24 | -------------------------------------------------------------------------------- /changelog.markdown: -------------------------------------------------------------------------------- 1 | # 1.1.0 No Log 2 | 3 | - Instead of logging an optional `done` callback is preferred instead 4 | 5 | # 1.0.0 IPO 6 | 7 | - Initial Public Release 8 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2015 Nicolas Bevacqua 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lipstick.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var net = require('net'); 4 | var path = require('path'); 5 | var cluster = require('cluster'); 6 | 7 | function lipstick (appfile, options, done) { 8 | var parsed = parse.apply(null, arguments); 9 | if (cluster.isMaster) { 10 | master(parsed.appfile, parsed.options, parsed.done); 11 | } else { 12 | throw new Error('workers shouldn\'t use lipstick!'); 13 | } 14 | } 15 | 16 | function parse (appfile, options, done) { 17 | var parsed = {}; 18 | var length = arguments.length; 19 | if (length === 1) { 20 | if (typeof appfile === 'function') { 21 | done = appfile; 22 | appfile = undefined; 23 | } else if (typeof appfile !== 'string') { 24 | options = appfile; 25 | appfile = undefined; 26 | } 27 | } 28 | if (length === 2) { 29 | if (typeof appfile === 'string') { 30 | if (typeof options === 'function') { 31 | done = options; 32 | options = undefined; 33 | } else { 34 | done = noop; 35 | } 36 | } else { 37 | done = options; 38 | options = appfile; 39 | appfile = undefined; 40 | } 41 | } 42 | appfile = appfile || './app.js'; 43 | options = options || {}; 44 | done = done || noop; 45 | parsed.appfile = appfile; 46 | parsed.options = options; 47 | parsed.done = done; 48 | return parsed; 49 | } 50 | 51 | function master (appfile, options, done) { 52 | var workerCount = options.workers || defaultWorkerCount(); 53 | var workers = []; 54 | var clusterOptions = { 55 | exec: path.resolve(appfile) 56 | } 57 | 58 | cluster.setupMaster(clusterOptions); 59 | spawnMany(); 60 | listen(); 61 | 62 | function spawnMany () { 63 | for (var i = 0; i < workerCount; i++) { 64 | spawn(i); 65 | } 66 | } 67 | function spawn (i) { 68 | workers[i] = cluster.fork(); 69 | workers[i].once('exit', exit); 70 | 71 | function exit (worker, code, signal) { 72 | workers.splice(i, 1, null); 73 | spawn(i); 74 | } 75 | } 76 | function connectionHandler (connection) { 77 | var worker = workers[hash(connection.remoteAddress, workerCount)]; 78 | worker.send('lipstick:connection', connection); 79 | } 80 | function listen () { 81 | var serverOptions = { pauseOnConnect: true }; 82 | var server = net.createServer(serverOptions, connectionHandler); 83 | var port = options.port || process.env.PORT; 84 | server.listen(port, listening); 85 | function listening () { 86 | done(null, port); 87 | } 88 | } 89 | } 90 | 91 | function defaultWorkerCount () { 92 | var cores = require('os').cpus().length; 93 | var count = Math.max(cores, 2); 94 | return count; 95 | } 96 | 97 | function hash (ip, len) { 98 | var parts = ''; 99 | for (var i = 0, _len = ip.length; i < _len; i++) { 100 | if (ip[i] >= '0' && ip[i] <= '9') { 101 | parts += ip[i]; 102 | } 103 | } 104 | return Number(parts) % len; 105 | } 106 | 107 | function listen (server, port, done) { 108 | if (cluster.isWorker) { 109 | server.listen(0, 'localhost', listening); 110 | } else { 111 | server.listen(port, done); 112 | } 113 | function listening () { 114 | process.on('message', message); 115 | done.apply(server, arguments); 116 | } 117 | function message (type, connection) { 118 | if (type !== 'lipstick:connection') { 119 | return; 120 | } 121 | server.emit('connection', connection); 122 | connection.resume(); 123 | } 124 | } 125 | 126 | function noop () { 127 | } 128 | 129 | lipstick.listen = listen; 130 | module.exports = lipstick; 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lipstick", 3 | "version": "1.1.0", 4 | "description": "Sticky sessions for Node.js clustering done responsibly", 5 | "main": "lipstick.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/bevacqua/lipstick.git" 9 | }, 10 | "author": "Nicolas Bevacqua (http://bevacqua.io/)", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/bevacqua/lipstick/issues" 14 | }, 15 | "homepage": "https://github.com/bevacqua/lipstick" 16 | } 17 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | # lipstick 2 | 3 | > Sticky sessions for Node.js clustering done responsibly 4 | 5 | # what is it? 6 | 7 | This module provides sticky sessions for Node.js at the cluster level, routing requests to the underlying workers by producing hashes very quickly. 8 | 9 | This module is an alternative to [sticky-session][4], usually most useful when running [Socket.IO >= `1.0.0`][5]. Remember to also set up the [socket.io-redis][6] adapter! 10 | 11 | # inspiration 12 | 13 | This module is inspired on the suggestions outlined by [elad][1] in their [`node-cluster-socket.io`][2] repository. All credit goes to them. 14 | 15 | I built upon the idea by adding support for IPv6, and attempted to streamline the implementation as much as possible for the `lipstick` consumers so that only minimal changes to your application are needed. 16 | 17 | # requirements 18 | 19 | - Node.js >= `0.12.x` for `pauseOnConnect` 20 | 21 | # install 22 | 23 | ```shell 24 | npm install lipstick --save 25 | ``` 26 | 27 | # usage 28 | 29 | To use `lipstick`, your master has to communicate with their workers effectively. 30 | 31 | ### master 32 | 33 | Here's a ready to use production-grade `cluster.js` file. It will listen on port `PORT` as defined in your environment variables. It will use `app.js` as your worker process and spawn [`os.cpus().length`][3] workers, or `2` of them, whichever is bigger. It will also route messages to the workers based on a hash of their IP address, applying the stickiness. 34 | 35 | ```js 36 | require('lipstick')(); 37 | ``` 38 | 39 | ### master api 40 | 41 | The API for your `cluster.js` module is detailed below. 42 | 43 | # `lipstick(appfile?, options?, done?)` 44 | 45 | The `appfile` defaults to `./app.js` and will be used as the worker process. Options are detailed below. 46 | 47 | Option | Description 48 | ----------|--------------------------------------------------------------------------------- 49 | `port` | The port your application listens on 50 | `workers` | The amount of workers your cluster should spawn 51 | 52 | ### workers 53 | 54 | You'll have to make a slight modification in your `app.js` workers. Just change the following line: 55 | 56 | ```js 57 | app.listen(port, cb); 58 | ``` 59 | 60 | To the `lipstick` equivalent shown below: 61 | 62 | ```js 63 | require('lipstick').listen(app, port, cb); 64 | ``` 65 | 66 | This will allow `lipstick` to patch your worker processes. `node app` will work as usual. 67 | 68 | # license 69 | 70 | MIT 71 | 72 | [1]: https://github.com/elad 73 | [2]: https://github.com/elad/node-cluster-socket.io 74 | [3]: https://nodejs.org/api/os.html#os_os_cpus 75 | [4]: https://github.com/indutny/sticky-session 76 | [5]: https://github.com/Automattic/socket.io 77 | [6]: https://github.com/Automattic/socket.io-redis 78 | --------------------------------------------------------------------------------