├── .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 [](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 |
--------------------------------------------------------------------------------