├── .gitignore ├── .gitmodules ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Makefile ├── README.md ├── faye-redis.js ├── package.json └── spec ├── faye_redis_spec.js ├── redis.conf └── runner.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.rdb 2 | node_modules 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/faye"] 2 | path = vendor/faye 3 | url = git://github.com/faye/faye.git 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .gitmodules 4 | .npmignore 5 | .travis.yml 6 | dump.rdb 7 | Makefile 8 | node_modules 9 | spec 10 | vendor 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "0.8" 5 | - "0.10" 6 | - "0.11" 7 | 8 | services: 9 | - redis-server 10 | 11 | before_install: 12 | - test $TRAVIS_NODE_VERSION = '0.8' && npm install -g npm@1.2.8000 || true 13 | 14 | before_script: 15 | - make 16 | 17 | env: TRAVIS=1 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 0.2.0 / 2013-10-01 2 | 3 | * Trigger the `close` event as required by Faye 1.0 4 | 5 | 6 | ### 0.1.3 / 2013-05-11 7 | 8 | * Fix a bug due to a misuse of `this` 9 | 10 | 11 | ### 0.1.2 / 2013-04-28 12 | 13 | * Improve garbage collection to avoid leaking Redis memory 14 | 15 | 16 | ### 0.1.1 / 2012-07-15 17 | 18 | * Fix an implicit global variable leak (missing semicolon) 19 | 20 | 21 | ### 0.1.0 / 2012-02-26 22 | 23 | * Initial release: Redis backend for Faye 0.8 24 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All projects under the [Faye](https://github.com/faye) umbrella are covered by 4 | the [Code of Conduct](https://github.com/faye/code-of-conduct). 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | prepare: 2 | git submodule update --init --recursive 3 | cd vendor/faye && npm install 4 | cd vendor/faye && ./node_modules/.bin/wake 5 | npm install 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # faye-redis [![Build Status](https://secure.travis-ci.org/faye/faye-redis-node.svg)](http://travis-ci.org/faye/faye-redis-node) 2 | 3 | This plugin provides a Redis-based backend for the 4 | [Faye](http://faye.jcoglan.com) messaging server. It allows a single Faye 5 | service to be distributed across many front-end web servers by storing state and 6 | routing messages through a [Redis](http://redis.io) database server. 7 | 8 | 9 | ## Usage 10 | 11 | Pass in the engine and any settings you need when setting up your Faye server. 12 | 13 | ```js 14 | var faye = require('faye'), 15 | redis = require('faye-redis'), 16 | http = require('http'); 17 | 18 | var server = http.createServer(); 19 | 20 | var bayeux = new faye.NodeAdapter({ 21 | mount: '/', 22 | timeout: 25, 23 | engine: { 24 | type: redis, 25 | host: 'redis.example.com', 26 | // more options 27 | } 28 | }); 29 | 30 | bayeux.attach(server); 31 | server.listen(8000); 32 | ``` 33 | 34 | The full list of settings is as follows. 35 | 36 | * `host` - hostname of your Redis instance 37 | * `port` - port number, default is `6379` 38 | * `password` - password, if `requirepass` is set 39 | * `database` - number of database to use, default is `0` 40 | * `namespace` - prefix applied to all keys, default is `''` 41 | * `socket` - path to Unix socket if `unixsocket` is set 42 | 43 | 44 | ## License 45 | 46 | (The MIT License) 47 | 48 | Copyright (c) 2011-2013 James Coglan 49 | 50 | Permission is hereby granted, free of charge, to any person obtaining a copy of 51 | this software and associated documentation files (the 'Software'), to deal in 52 | the Software without restriction, including without limitation the rights to 53 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 54 | the Software, and to permit persons to whom the Software is furnished to do so, 55 | subject to the following conditions: 56 | 57 | The above copyright notice and this permission notice shall be included in all 58 | copies or substantial portions of the Software. 59 | 60 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 61 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 62 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 63 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 64 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 65 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 66 | -------------------------------------------------------------------------------- /faye-redis.js: -------------------------------------------------------------------------------- 1 | var Engine = function(server, options) { 2 | this._server = server; 3 | this._options = options || {}; 4 | 5 | var redis = require('redis'), 6 | host = this._options.host || this.DEFAULT_HOST, 7 | port = this._options.port || this.DEFAULT_PORT, 8 | db = this._options.database || this.DEFAULT_DATABASE, 9 | auth = this._options.password, 10 | gc = this._options.gc || this.DEFAULT_GC, 11 | socket = this._options.socket; 12 | 13 | this._ns = this._options.namespace || ''; 14 | 15 | if (socket) { 16 | this._redis = redis.createClient(socket, {no_ready_check: true}); 17 | this._subscriber = redis.createClient(socket, {no_ready_check: true}); 18 | } else { 19 | this._redis = redis.createClient(port, host, {no_ready_check: true}); 20 | this._subscriber = redis.createClient(port, host, {no_ready_check: true}); 21 | } 22 | 23 | if (auth) { 24 | this._redis.auth(auth); 25 | this._subscriber.auth(auth); 26 | } 27 | this._redis.select(db); 28 | this._subscriber.select(db); 29 | 30 | this._messageChannel = this._ns + '/notifications/messages'; 31 | this._closeChannel = this._ns + '/notifications/close'; 32 | 33 | var self = this; 34 | this._subscriber.subscribe(this._messageChannel); 35 | this._subscriber.subscribe(this._closeChannel); 36 | this._subscriber.on('message', function(topic, message) { 37 | if (topic === self._messageChannel) self.emptyQueue(message); 38 | if (topic === self._closeChannel) self._server.trigger('close', message); 39 | }); 40 | 41 | this._gc = setInterval(function() { self.gc() }, gc * 1000); 42 | }; 43 | 44 | Engine.create = function(server, options) { 45 | return new this(server, options); 46 | }; 47 | 48 | Engine.prototype = { 49 | DEFAULT_HOST: 'localhost', 50 | DEFAULT_PORT: 6379, 51 | DEFAULT_DATABASE: 0, 52 | DEFAULT_GC: 60, 53 | LOCK_TIMEOUT: 120, 54 | 55 | disconnect: function() { 56 | this._redis.end(); 57 | this._subscriber.unsubscribe(); 58 | this._subscriber.end(); 59 | clearInterval(this._gc); 60 | }, 61 | 62 | createClient: function(callback, context) { 63 | var clientId = this._server.generateId(), self = this; 64 | this._redis.zadd(this._ns + '/clients', 0, clientId, function(error, added) { 65 | if (added === 0) return self.createClient(callback, context); 66 | self._server.debug('Created new client ?', clientId); 67 | self.ping(clientId); 68 | self._server.trigger('handshake', clientId); 69 | callback.call(context, clientId); 70 | }); 71 | }, 72 | 73 | clientExists: function(clientId, callback, context) { 74 | var cutoff = new Date().getTime() - (1000 * 1.6 * this._server.timeout); 75 | 76 | this._redis.zscore(this._ns + '/clients', clientId, function(error, score) { 77 | callback.call(context, parseInt(score, 10) > cutoff); 78 | }); 79 | }, 80 | 81 | destroyClient: function(clientId, callback, context) { 82 | var self = this; 83 | 84 | this._redis.smembers(this._ns + '/clients/' + clientId + '/channels', function(error, channels) { 85 | var multi = self._redis.multi(); 86 | 87 | multi.zadd(self._ns + '/clients', 0, clientId); 88 | 89 | channels.forEach(function(channel) { 90 | multi.srem(self._ns + '/clients/' + clientId + '/channels', channel); 91 | multi.srem(self._ns + '/channels' + channel, clientId); 92 | }); 93 | multi.del(self._ns + '/clients/' + clientId + '/messages'); 94 | multi.zrem(self._ns + '/clients', clientId); 95 | multi.publish(self._closeChannel, clientId); 96 | 97 | multi.exec(function(error, results) { 98 | channels.forEach(function(channel, i) { 99 | if (results[2 * i + 1] !== 1) return; 100 | self._server.trigger('unsubscribe', clientId, channel); 101 | self._server.debug('Unsubscribed client ? from channel ?', clientId, channel); 102 | }); 103 | 104 | self._server.debug('Destroyed client ?', clientId); 105 | self._server.trigger('disconnect', clientId); 106 | 107 | if (callback) callback.call(context); 108 | }); 109 | }); 110 | }, 111 | 112 | ping: function(clientId) { 113 | var timeout = this._server.timeout; 114 | if (typeof timeout !== 'number') return; 115 | 116 | var time = new Date().getTime(); 117 | 118 | this._server.debug('Ping ?, ?', clientId, time); 119 | this._redis.zadd(this._ns + '/clients', time, clientId); 120 | }, 121 | 122 | subscribe: function(clientId, channel, callback, context) { 123 | var self = this; 124 | this._redis.sadd(this._ns + '/clients/' + clientId + '/channels', channel, function(error, added) { 125 | if (added === 1) self._server.trigger('subscribe', clientId, channel); 126 | }); 127 | this._redis.sadd(this._ns + '/channels' + channel, clientId, function() { 128 | self._server.debug('Subscribed client ? to channel ?', clientId, channel); 129 | if (callback) callback.call(context); 130 | }); 131 | }, 132 | 133 | unsubscribe: function(clientId, channel, callback, context) { 134 | var self = this; 135 | this._redis.srem(this._ns + '/clients/' + clientId + '/channels', channel, function(error, removed) { 136 | if (removed === 1) self._server.trigger('unsubscribe', clientId, channel); 137 | }); 138 | this._redis.srem(this._ns + '/channels' + channel, clientId, function() { 139 | self._server.debug('Unsubscribed client ? from channel ?', clientId, channel); 140 | if (callback) callback.call(context); 141 | }); 142 | }, 143 | 144 | publish: function(message, channels) { 145 | this._server.debug('Publishing message ?', message); 146 | 147 | var self = this, 148 | jsonMessage = JSON.stringify(message), 149 | keys = channels.map(function(c) { return self._ns + '/channels' + c }); 150 | 151 | var notify = function(error, clients) { 152 | clients.forEach(function(clientId) { 153 | var queue = self._ns + '/clients/' + clientId + '/messages'; 154 | 155 | self._server.debug('Queueing for client ?: ?', clientId, message); 156 | self._redis.rpush(queue, jsonMessage); 157 | self._redis.publish(self._messageChannel, clientId); 158 | 159 | self.clientExists(clientId, function(exists) { 160 | if (!exists) self._redis.del(queue); 161 | }); 162 | }); 163 | }; 164 | keys.push(notify); 165 | this._redis.sunion.apply(this._redis, keys); 166 | 167 | this._server.trigger('publish', message.clientId, message.channel, message.data); 168 | }, 169 | 170 | emptyQueue: function(clientId) { 171 | if (!this._server.hasConnection(clientId)) return; 172 | 173 | var key = this._ns + '/clients/' + clientId + '/messages', 174 | multi = this._redis.multi(), 175 | self = this; 176 | 177 | multi.lrange(key, 0, -1, function(error, jsonMessages) { 178 | if (!jsonMessages) return; 179 | var messages = jsonMessages.map(function(json) { return JSON.parse(json) }); 180 | self._server.deliver(clientId, messages); 181 | }); 182 | multi.del(key); 183 | multi.exec(); 184 | }, 185 | 186 | gc: function() { 187 | var timeout = this._server.timeout; 188 | if (typeof timeout !== 'number') return; 189 | 190 | this._withLock('gc', function(releaseLock) { 191 | var cutoff = new Date().getTime() - 1000 * 2 * timeout, 192 | self = this; 193 | 194 | this._redis.zrangebyscore(this._ns + '/clients', 0, cutoff, function(error, clients) { 195 | var i = 0, n = clients.length; 196 | if (i === n) return releaseLock(); 197 | 198 | clients.forEach(function(clientId) { 199 | this.destroyClient(clientId, function() { 200 | i += 1; 201 | if (i === n) releaseLock(); 202 | }, this); 203 | }, self); 204 | }); 205 | }, this); 206 | }, 207 | 208 | _withLock: function(lockName, callback, context) { 209 | var lockKey = this._ns + '/locks/' + lockName, 210 | currentTime = new Date().getTime(), 211 | expiry = currentTime + this.LOCK_TIMEOUT * 1000 + 1, 212 | self = this; 213 | 214 | var releaseLock = function() { 215 | if (new Date().getTime() < expiry) self._redis.del(lockKey); 216 | }; 217 | 218 | this._redis.setnx(lockKey, expiry, function(error, set) { 219 | if (set === 1) return callback.call(context, releaseLock); 220 | 221 | self._redis.get(lockKey, function(error, timeout) { 222 | if (!timeout) return; 223 | 224 | var lockTimeout = parseInt(timeout, 10); 225 | if (currentTime < lockTimeout) return; 226 | 227 | self._redis.getset(lockKey, expiry, function(error, oldValue) { 228 | if (oldValue !== timeout) return; 229 | callback.call(context, releaseLock); 230 | }); 231 | }); 232 | }); 233 | } 234 | }; 235 | 236 | module.exports = Engine; 237 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name" : "faye-redis" 2 | , "description" : "Redis backend engine for Faye" 3 | , "homepage" : "http://github.com/faye/faye-redis-node" 4 | , "author" : "James Coglan (http://jcoglan.com/)" 5 | , "keywords" : ["pubsub", "bayeux"] 6 | , "license" : "MIT" 7 | 8 | , "version" : "0.2.0" 9 | , "engines" : {"node": ">=0.4.0"} 10 | , "main" : "./faye-redis" 11 | , "dependencies" : {"redis": ""} 12 | , "devDependencies" : {"jstest": ""} 13 | 14 | , "scripts" : {"test": "node spec/runner.js"} 15 | 16 | , "bugs" : "http://github.com/faye/faye-redis-node/issues" 17 | 18 | , "repository" : { "type" : "git" 19 | , "url" : "git://github.com/faye/faye-redis-node.git" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /spec/faye_redis_spec.js: -------------------------------------------------------------------------------- 1 | var RedisEngine = require('../faye-redis') 2 | 3 | JS.Test.describe("Redis engine", function() { with(this) { 4 | before(function() { 5 | var pw = process.env.TRAVIS ? undefined : "foobared" 6 | this.engineOpts = {type: RedisEngine, password: pw, namespace: new Date().getTime().toString()} 7 | }) 8 | 9 | after(function(resume) { with(this) { 10 | disconnect_engine() 11 | var redis = require('redis').createClient(6379, 'localhost', {no_ready_check: true}) 12 | redis.auth(engineOpts.password) 13 | redis.flushall(function() { 14 | redis.end() 15 | resume() 16 | }) 17 | }}) 18 | 19 | itShouldBehaveLike("faye engine") 20 | 21 | describe("distribution", function() { with(this) { 22 | itShouldBehaveLike("distributed engine") 23 | }}) 24 | 25 | if (process.env.TRAVIS) return 26 | 27 | describe("using a Unix socket", function() { with(this) { 28 | before(function() { with(this) { 29 | this.engineOpts.socket = "/tmp/redis.sock" 30 | }}) 31 | 32 | itShouldBehaveLike("faye engine") 33 | }}) 34 | }}) 35 | -------------------------------------------------------------------------------- /spec/redis.conf: -------------------------------------------------------------------------------- 1 | daemonize no 2 | pidfile /tmp/redis.pid 3 | port 6379 4 | unixsocket /tmp/redis.sock 5 | timeout 300 6 | loglevel verbose 7 | logfile stdout 8 | databases 16 9 | 10 | save 900 1 11 | save 300 10 12 | save 60 10000 13 | 14 | rdbcompression yes 15 | dbfilename dump.rdb 16 | dir ./ 17 | 18 | slave-serve-stale-data yes 19 | 20 | requirepass foobared 21 | 22 | appendonly no 23 | appendfsync everysec 24 | no-appendfsync-on-rewrite no 25 | 26 | vm-enabled no 27 | vm-swap-file /tmp/redis.swap 28 | vm-max-memory 0 29 | vm-page-size 32 30 | vm-pages 134217728 31 | vm-max-threads 4 32 | 33 | hash-max-zipmap-entries 512 34 | hash-max-zipmap-value 64 35 | 36 | list-max-ziplist-entries 512 37 | list-max-ziplist-value 64 38 | 39 | set-max-intset-entries 512 40 | 41 | activerehashing yes 42 | -------------------------------------------------------------------------------- /spec/runner.js: -------------------------------------------------------------------------------- 1 | JS = require('jstest') 2 | 3 | Faye = require('../vendor/faye/build/node/faye-node') 4 | require('../vendor/faye/spec/javascript/engine_spec') 5 | require('./faye_redis_spec') 6 | 7 | JS.Test.autorun() 8 | --------------------------------------------------------------------------------