Skip to content

Commit 05f926e

Browse files
[perf] Use pattern matching at the namespace level (#217)
This follows #46. Each node will now listen to only three channels: - `socket.io#<namespace>#*`: used when broadcasting - `socket.io-request#<namespace>#`: used for requesting information (ex: get every room in the cluster) - `socket.io-response#<namespace>#`: used for responding to requests We keep the benefits of #46 since: - messages from other namespaces are ignored - when emitting to a single room, the message is sent to `socket.io#<namespace>#<my-room>`, so listeners can check whether they have the room before unpacking the message (which is CPU consuming). But there is no need to subscribe / unsubscribe every time a socket joins or leaves a room (which is also CPU consuming when there are thousands of subscriptions).
1 parent d3d000b commit 05f926e

File tree

5 files changed

+58
-133
lines changed

5 files changed

+58
-133
lines changed

.travis.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
language: node_js
22
sudo: false
33
node_js:
4-
- "0.10"
5-
- "0.12"
64
- "4"
75
- "6"
86
- "node"

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,9 @@ The following options are allowed:
3232
- `key`: the name of the key to pub/sub events on as prefix (`socket.io`)
3333
- `host`: host to connect to redis on (`localhost`)
3434
- `port`: port to connect to redis on (`6379`)
35-
- `subEvent`: optional, the redis client event name to subscribe to (`messageBuffer`)
3635
- `pubClient`: optional, the redis client to publish events on
3736
- `subClient`: optional, the redis client to subscribe to events on
3837
- `requestsTimeout`: optional, after this timeout the adapter will stop waiting from responses to request (`1000ms`)
39-
- `withChannelMultiplexing`: optional, whether channel multiplexing is enabled (a new subscription will be trigggered for each room) (`true`)
4038

4139
If you decide to supply `pubClient` and `subClient`, make sure you use
4240
[node_redis](https://github.com/mranney/node_redis) as a client or one

index.js

Lines changed: 29 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ var redis = require('redis').createClient;
88
var msgpack = require('msgpack-lite');
99
var Adapter = require('socket.io-adapter');
1010
var debug = require('debug')('socket.io-redis');
11-
var async = require('async');
1211

1312
/**
1413
* Module exports.
@@ -50,11 +49,8 @@ function adapter(uri, opts) {
5049
// opts
5150
var pub = opts.pubClient;
5251
var sub = opts.subClient;
53-
5452
var prefix = opts.key || 'socket.io';
55-
var subEvent = opts.subEvent || 'messageBuffer';
5653
var requestsTimeout = opts.requestsTimeout || 1000;
57-
var withChannelMultiplexing = false !== opts.withChannelMultiplexing;
5854

5955
// init clients if needed
6056
function createClient() {
@@ -85,7 +81,6 @@ function adapter(uri, opts) {
8581
this.uid = uid;
8682
this.prefix = prefix;
8783
this.requestsTimeout = requestsTimeout;
88-
this.withChannelMultiplexing = withChannelMultiplexing;
8984

9085
this.channel = prefix + '#' + nsp.name + '#';
9186
this.requestChannel = prefix + '-request#' + this.nsp.name + '#';
@@ -107,11 +102,17 @@ function adapter(uri, opts) {
107102

108103
var self = this;
109104

110-
sub.subscribe([this.channel, this.requestChannel, this.responseChannel], function(err){
105+
sub.psubscribe(this.channel + '*', function(err){
106+
if (err) self.emit('error', err);
107+
});
108+
109+
sub.on('pmessageBuffer', this.onmessage.bind(this));
110+
111+
sub.subscribe([this.requestChannel, this.responseChannel], function(err){
111112
if (err) self.emit('error', err);
112113
});
113114

114-
sub.on(subEvent, this.onmessage.bind(this));
115+
sub.on('messageBuffer', this.onrequest.bind(this));
115116

116117
function onError(err) {
117118
self.emit('error', err);
@@ -132,21 +133,22 @@ function adapter(uri, opts) {
132133
* @api private
133134
*/
134135

135-
Redis.prototype.onmessage = function(channel, msg){
136+
Redis.prototype.onmessage = function(pattern, channel, msg){
136137
channel = channel.toString();
137138

138-
if (this.channelMatches(channel, this.requestChannel)) {
139-
return this.onrequest(channel, msg);
140-
} else if (this.channelMatches(channel, this.responseChannel)) {
141-
return this.onresponse(channel, msg);
142-
} else if (!this.channelMatches(channel, this.channel)) {
139+
if (!this.channelMatches(channel, this.channel)) {
143140
return debug('ignore different channel');
144141
}
145142

143+
var room = channel.substring(this.channel.length);
144+
if (room !== '' && !this.rooms.hasOwnProperty(room)) {
145+
return debug('ignore unknown room %s', room);
146+
}
147+
146148
var args = msgpack.decode(msg);
147149
var packet;
148150

149-
if (uid == args.shift()) return debug('ignore same uid');
151+
if (uid === args.shift()) return debug('ignore same uid');
150152

151153
packet = args[0];
152154

@@ -170,6 +172,14 @@ function adapter(uri, opts) {
170172
*/
171173

172174
Redis.prototype.onrequest = function(channel, msg){
175+
channel = channel.toString();
176+
177+
if (this.channelMatches(channel, this.responseChannel)) {
178+
return this.onresponse(channel, msg);
179+
} else if (!this.channelMatches(channel, this.requestChannel)) {
180+
return debug('ignore different channel');
181+
}
182+
173183
var self = this;
174184
var request;
175185

@@ -394,116 +404,15 @@ function adapter(uri, opts) {
394404
packet.nsp = this.nsp.name;
395405
if (!(remote || (opts && opts.flags && opts.flags.local))) {
396406
var msg = msgpack.encode([uid, packet, opts]);
397-
if (this.withChannelMultiplexing && opts.rooms && opts.rooms.length === 1) {
398-
pub.publish(this.channel + opts.rooms[0] + '#', msg);
407+
if (opts.rooms && opts.rooms.length === 1) {
408+
pub.publish(this.channel + opts.rooms[0], msg);
399409
} else {
400410
pub.publish(this.channel, msg);
401411
}
402412
}
403413
Adapter.prototype.broadcast.call(this, packet, opts);
404414
};
405415

406-
/**
407-
* Subscribe client to room messages.
408-
*
409-
* @param {String} client id
410-
* @param {String} room
411-
* @param {Function} callback (optional)
412-
* @api public
413-
*/
414-
415-
Redis.prototype.add = function(id, room, fn){
416-
debug('adding %s to %s ', id, room);
417-
var self = this;
418-
// subscribe only once per room
419-
var alreadyHasRoom = this.rooms.hasOwnProperty(room);
420-
Adapter.prototype.add.call(this, id, room);
421-
422-
if (!this.withChannelMultiplexing || alreadyHasRoom) {
423-
if (fn) fn(null);
424-
return;
425-
}
426-
427-
var channel = this.channel + room + '#';
428-
429-
function onSubscribe(err) {
430-
if (err) {
431-
self.emit('error', err);
432-
if (fn) fn(err);
433-
return;
434-
}
435-
if (fn) fn(null);
436-
}
437-
438-
sub.subscribe(channel, onSubscribe);
439-
};
440-
441-
/**
442-
* Unsubscribe client from room messages.
443-
*
444-
* @param {String} session id
445-
* @param {String} room id
446-
* @param {Function} callback (optional)
447-
* @api public
448-
*/
449-
450-
Redis.prototype.del = function(id, room, fn){
451-
debug('removing %s from %s', id, room);
452-
453-
var self = this;
454-
var hasRoom = this.rooms.hasOwnProperty(room);
455-
Adapter.prototype.del.call(this, id, room);
456-
457-
if (this.withChannelMultiplexing && hasRoom && !this.rooms[room]) {
458-
var channel = this.channel + room + '#';
459-
460-
function onUnsubscribe(err) {
461-
if (err) {
462-
self.emit('error', err);
463-
if (fn) fn(err);
464-
return;
465-
}
466-
if (fn) fn(null);
467-
}
468-
469-
sub.unsubscribe(channel, onUnsubscribe);
470-
} else {
471-
if (fn) process.nextTick(fn.bind(null, null));
472-
}
473-
};
474-
475-
/**
476-
* Unsubscribe client completely.
477-
*
478-
* @param {String} client id
479-
* @param {Function} callback (optional)
480-
* @api public
481-
*/
482-
483-
Redis.prototype.delAll = function(id, fn){
484-
debug('removing %s from all rooms', id);
485-
486-
var self = this;
487-
var rooms = this.sids[id];
488-
489-
if (!rooms) {
490-
if (fn) process.nextTick(fn.bind(null, null));
491-
return;
492-
}
493-
494-
async.each(Object.keys(rooms), function(room, next){
495-
self.del(id, room, next);
496-
}, function(err){
497-
if (err) {
498-
self.emit('error', err);
499-
if (fn) fn(err);
500-
return;
501-
}
502-
delete self.sids[id];
503-
if (fn) fn(null);
504-
});
505-
};
506-
507416
/**
508417
* Gets a list of clients by sid.
509418
*
@@ -531,6 +440,7 @@ function adapter(uri, opts) {
531440
}
532441

533442
numsub = parseInt(numsub[1], 10);
443+
debug('waiting for %d responses to "clients" request', numsub);
534444

535445
var request = JSON.stringify({
536446
requestid : requestid,
@@ -619,6 +529,7 @@ function adapter(uri, opts) {
619529
}
620530

621531
numsub = parseInt(numsub[1], 10);
532+
debug('waiting for %d responses to "allRooms" request', numsub);
622533

623534
var request = JSON.stringify({
624535
requestid : requestid,
@@ -794,6 +705,7 @@ function adapter(uri, opts) {
794705
}
795706

796707
numsub = parseInt(numsub[1], 10);
708+
debug('waiting for %d responses to "customRequest" request', numsub);
797709

798710
var request = JSON.stringify({
799711
requestid : requestid,

package.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,17 @@
1414
"test": "mocha"
1515
},
1616
"dependencies": {
17-
"async": "2.1.4",
1817
"debug": "2.3.3",
1918
"msgpack-lite": "0.1.26",
2019
"redis": "2.6.3",
21-
"socket.io-adapter": "0.5.0",
20+
"socket.io-adapter": "~1.1.0",
2221
"uid2": "0.0.3"
2322
},
2423
"devDependencies": {
2524
"expect.js": "0.3.1",
2625
"ioredis": "2.5.0",
2726
"mocha": "3.2.0",
28-
"socket.io": "1.7.x",
29-
"socket.io-client": "1.7.x"
27+
"socket.io": "latest",
28+
"socket.io-client": "latest"
3029
}
3130
}

test/index.js

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,6 @@ var socket1, socket2, socket3;
1515
{
1616
name: 'socket.io-redis'
1717
},
18-
{
19-
name: 'socket.io-redis without channel multiplexing',
20-
options: {
21-
withChannelMultiplexing: false
22-
}
23-
},
2418
{
2519
name: 'socket.io-redis with ioredis',
2620
options: function () {
@@ -152,7 +146,7 @@ var socket1, socket2, socket3;
152146
it('deletes rooms upon disconnection', function(done){
153147
socket1.join('woot');
154148
socket1.on('disconnect', function() {
155-
expect(socket1.adapter.sids[socket1.id]).to.be.empty();
149+
expect(socket1.adapter.sids[socket1.id]).to.be(undefined);
156150
expect(socket1.adapter.rooms).to.be.empty();
157151
client1.disconnect();
158152
done();
@@ -175,6 +169,26 @@ var socket1, socket2, socket3;
175169
});
176170
});
177171

172+
it('ignores messages from unknown channels', function(done){
173+
namespace1.adapter.subClient.psubscribe('f?o', function () {
174+
namespace3.adapter.pubClient.publish('foo', 'bar');
175+
});
176+
177+
namespace1.adapter.subClient.on('pmessageBuffer', function () {
178+
setTimeout(done, 50);
179+
});
180+
});
181+
182+
it('ignores messages from unknown channels (2)', function(done){
183+
namespace1.adapter.subClient.subscribe('woot', function () {
184+
namespace3.adapter.pubClient.publish('woot', 'toow');
185+
});
186+
187+
namespace1.adapter.subClient.on('messageBuffer', function () {
188+
setTimeout(done, 50);
189+
});
190+
});
191+
178192
describe('rooms', function () {
179193
it('returns rooms of a given client', function(done){
180194
socket1.join('woot1', function () {
@@ -200,6 +214,7 @@ var socket1, socket2, socket3;
200214
it('returns all rooms accross several nodes', function(done){
201215
socket1.join('woot1', function () {
202216
namespace1.adapter.allRooms(function(err, rooms){
217+
expect(err).to.be(null);
203218
expect(rooms).to.have.length(4);
204219
expect(rooms).to.contain(socket1.id);
205220
expect(rooms).to.contain(socket2.id);
@@ -212,6 +227,7 @@ var socket1, socket2, socket3;
212227

213228
it('makes a given socket join a room', function(done){
214229
namespace3.adapter.remoteJoin(socket1.id, 'woot3', function(err){
230+
expect(err).to.be(null);
215231
var rooms = Object.keys(socket1.rooms);
216232
expect(rooms).to.have.length(2);
217233
expect(rooms).to.contain('woot3');
@@ -222,6 +238,7 @@ var socket1, socket2, socket3;
222238
it('makes a given socket leave a room', function(done){
223239
socket1.join('woot3', function(){
224240
namespace3.adapter.remoteLeave(socket1.id, 'woot3', function(err){
241+
expect(err).to.be(null);
225242
var rooms = Object.keys(socket1.rooms);
226243
expect(rooms).to.have.length(1);
227244
expect(rooms).not.to.contain('woot3');
@@ -237,6 +254,7 @@ var socket1, socket2, socket3;
237254
}
238255

239256
namespace3.adapter.customRequest('hello', function(err, replies){
257+
expect(err).to.be(null);
240258
expect(replies).to.have.length(3);
241259
expect(replies).to.contain(namespace1.adapter.uid);
242260
done();
@@ -297,7 +315,7 @@ function init(options){
297315
socket1 = _socket1;
298316
socket2 = _socket2;
299317
socket3 = _socket3;
300-
done();
318+
setTimeout(done, 100);
301319
});
302320
});
303321
});

0 commit comments

Comments
 (0)