Browse Source

Merge pull request #1017 from NodeRedis/pubsub

Fix pub sub mode

Fixes #603 
Fixes #577 
Fixes #137
internal
Ruben Bridgewater 9 years ago
parent
commit
50f1663ba5
  1. 38
      README.md
  2. 8
      changelog.md
  3. 232
      index.js
  4. 14
      lib/command.js
  5. 26
      test/auth.spec.js
  6. 2
      test/commands/hgetall.spec.js
  7. 3
      test/connection.spec.js
  8. 75
      test/node_redis.spec.js
  9. 262
      test/pubsub.spec.js

38
README.md

@ -233,7 +233,7 @@ client.get("foo_rand000000000000", function (err, reply) {
client.get(new Buffer("foo_rand000000000000"), function (err, reply) {
console.log(reply.toString()); // Will print `<Buffer 4f 4b>`
});
client.end();
client.quit();
```
retry_strategy example
@ -302,7 +302,7 @@ client.get("foo_rand000000000000", function (err, reply) {
});
```
`client.end()` without the flush parameter should NOT be used in production!
`client.end()` without the flush parameter set to true should NOT be used in production!
## client.unref()
@ -377,34 +377,34 @@ client connections, subscribes to a channel on one of them, and publishes to tha
channel on the other:
```js
var redis = require("redis"),
client1 = redis.createClient(), client2 = redis.createClient(),
msg_count = 0;
var redis = require("redis");
var sub = redis.createClient(), pub = redis.createClient();
var msg_count = 0;
client1.on("subscribe", function (channel, count) {
client2.publish("a nice channel", "I am sending a message.");
client2.publish("a nice channel", "I am sending a second message.");
client2.publish("a nice channel", "I am sending my last message.");
sub.on("subscribe", function (channel, count) {
pub.publish("a nice channel", "I am sending a message.");
pub.publish("a nice channel", "I am sending a second message.");
pub.publish("a nice channel", "I am sending my last message.");
});
client1.on("message", function (channel, message) {
console.log("client1 channel " + channel + ": " + message);
sub.on("message", function (channel, message) {
console.log("sub channel " + channel + ": " + message);
msg_count += 1;
if (msg_count === 3) {
client1.unsubscribe();
client1.end();
client2.end();
sub.unsubscribe();
sub.quit();
pub.quit();
}
});
client1.subscribe("a nice channel");
sub.subscribe("a nice channel");
```
When a client issues a `SUBSCRIBE` or `PSUBSCRIBE`, that connection is put into a "subscriber" mode.
At that point, only commands that modify the subscription set are valid. When the subscription
At that point, only commands that modify the subscription set are valid and quit (and depending on the redis version ping as well). When the subscription
set is empty, the connection is put back into regular mode.
If you need to send regular commands to Redis while in subscriber mode, just open another connection.
If you need to send regular commands to Redis while in subscriber mode, just open another connection with a new client (hint: use `client.duplicate()`).
## Subscriber Events
@ -413,13 +413,13 @@ If a client has subscriptions active, it may emit these events:
### "message" (channel, message)
Client will emit `message` for every message received that matches an active subscription.
Listeners are passed the channel name as `channel` and the message Buffer as `message`.
Listeners are passed the channel name as `channel` and the message as `message`.
### "pmessage" (pattern, channel, message)
Client will emit `pmessage` for every message received that matches an active subscription pattern.
Listeners are passed the original pattern used with `PSUBSCRIBE` as `pattern`, the sending channel
name as `channel`, and the message Buffer as `message`.
name as `channel`, and the message as `message`.
### "subscribe" (channel, count)

8
changelog.md

@ -5,15 +5,21 @@ Changelog
Features
- Monitor now works together with the offline queue
- Monitor and pub sub mode now work together with the offline queue
- All commands that were send after a connection loss are now going to be send after reconnecting
- Activating monitor mode does now work together with arbitrary commands including pub sub mode
- Pub sub mode is completly rewritten and all known issues fixed
Bugfixes
- Fixed calling monitor command while other commands are still running
- Fixed monitor and pub sub mode not working together
- Fixed monitor mode not working in combination with the offline queue
- Fixed pub sub mode not working in combination with the offline queue
- Fixed pub sub mode resubscribing not working with non utf8 buffer channels
- Fixed pub sub mode crashing if calling unsubscribe / subscribe in various combinations
- Fixed pub sub mode emitting unsubscribe even if no channels were unsubscribed
- Fixed pub sub mode emitting a message without a message published
## v.2.5.3 - 21 Mar, 2016

232
index.js

@ -5,7 +5,8 @@ var tls = require('tls');
var util = require('util');
var utils = require('./lib/utils');
var Queue = require('double-ended-queue');
var Command = require('./lib/command');
var Command = require('./lib/command').Command;
var OfflineCommand = require('./lib/command').OfflineCommand;
var EventEmitter = require('events');
var Parser = require('redis-parser');
var commands = require('redis-commands');
@ -128,7 +129,7 @@ function RedisClient (options, stream) {
);
}
this.initialize_retry_vars();
this.pub_sub_mode = false;
this.pub_sub_mode = 0;
this.subscription_set = {};
this.monitoring = false;
this.closing = false;
@ -222,6 +223,7 @@ RedisClient.prototype.create_stream = function () {
// The buffer_from_socket.toString() has a significant impact on big chunks and therefor this should only be used if necessary
debug('Net read ' + self.address + ' id ' + self.connection_id); // + ': ' + buffer_from_socket.toString());
self.reply_parser.execute(buffer_from_socket);
self.emit_idle();
});
this.stream.on('error', function (err) {
@ -386,7 +388,7 @@ RedisClient.prototype.on_ready = function () {
}
this.cork = cork;
// restore modal commands from previous connection. The order of the commands is important
// Restore modal commands from previous connection. The order of the commands is important
if (this.selected_db !== undefined) {
this.send_command('select', [this.selected_db]);
}
@ -394,31 +396,29 @@ RedisClient.prototype.on_ready = function () {
this.monitoring = this.old_state.monitoring;
this.pub_sub_mode = this.old_state.pub_sub_mode;
}
if (this.pub_sub_mode) {
if (this.monitoring) { // Monitor has to be fired before pub sub commands
this.send_command('monitor', []);
}
var callback_count = Object.keys(this.subscription_set).length;
if (!this.options.disable_resubscribing && callback_count) {
// only emit 'ready' when all subscriptions were made again
var callback_count = 0;
// TODO: Remove the countdown for ready here. This is not coherent with all other modes and should therefor not be handled special
// We know we are ready as soon as all commands were fired
var callback = function () {
callback_count--;
if (callback_count === 0) {
self.emit('ready');
}
};
if (this.options.disable_resubscribing) {
this.emit('ready');
return;
debug('Sending pub/sub on_ready commands');
for (var key in this.subscription_set) { // jshint ignore: line
var command = key.slice(0, key.indexOf('_'));
var args = self.subscription_set[key];
self.send_command(command, [args], callback);
}
Object.keys(this.subscription_set).forEach(function (key) {
var space_index = key.indexOf(' ');
var parts = [key.slice(0, space_index), key.slice(space_index + 1)];
debug('Sending pub/sub on_ready ' + parts[0] + ', ' + parts[1]);
callback_count++;
self.send_command(parts[0] + 'scribe', [parts[1]], callback);
});
this.send_offline_queue();
return;
}
if (this.monitoring) {
this.send_command('monitor', []);
}
this.send_offline_queue();
this.emit('ready');
};
@ -521,7 +521,7 @@ RedisClient.prototype.connection_gone = function (why, error) {
};
this.old_state = state;
this.monitoring = false;
this.pub_sub_mode = false;
this.pub_sub_mode = 0;
// since we are collapsing end and close, users don't expect to be called twice
if (!this.emitted_end) {
@ -603,7 +603,6 @@ RedisClient.prototype.return_error = function (err) {
err.code = match[1];
}
this.emit_idle();
utils.callback_or_emit(this, command_obj && command_obj.callback, err);
};
@ -613,19 +612,13 @@ RedisClient.prototype.drain = function () {
};
RedisClient.prototype.emit_idle = function () {
if (this.command_queue.length === 0 && this.pub_sub_mode === false) {
if (this.command_queue.length === 0 && this.pub_sub_mode === 0) {
this.emit('idle');
}
};
/* istanbul ignore next: this is a safety check that we should not be able to trigger */
function queue_state_error (self, command_obj) {
var err = new Error('node_redis command queue state error. If you can reproduce this, please report it.');
err.command_obj = command_obj;
self.emit('error', err);
}
function normal_reply (self, reply, command_obj) {
function normal_reply (self, reply) {
var command_obj = self.command_queue.shift();
if (typeof command_obj.callback === 'function') {
if ('exec' !== command_obj.command) {
reply = self.handle_reply(reply, command_obj.command, command_obj.buffer_args);
@ -636,67 +629,107 @@ function normal_reply (self, reply, command_obj) {
}
}
function return_pub_sub (self, reply, command_obj) {
if (reply instanceof Array) {
if ((!command_obj || command_obj.buffer_args === false) && !self.options.return_buffers) {
reply = utils.reply_to_strings(reply);
function set_subscribe (self, type, command_obj, subscribe, reply) {
var i = 0;
if (subscribe) {
// The channels have to be saved one after the other and the type has to be the same too,
// to make sure partly subscribe / unsubscribe works well together
for (; i < command_obj.args.length; i++) {
self.subscription_set[type + '_' + command_obj.args[i]] = command_obj.args[i];
}
var type = reply[0].toString();
// TODO: Add buffer emiters (we have to get all pubsub messages as buffers back in that case)
if (type === 'message') {
self.emit('message', reply[1], reply[2]); // channcel, message
} else if (type === 'pmessage') {
self.emit('pmessage', reply[1], reply[2], reply[3]); // pattern, channcel, message
} else if (type === 'subscribe' || type === 'unsubscribe' || type === 'psubscribe' || type === 'punsubscribe') {
if (reply[2].toString() === '0') {
self.pub_sub_mode = false;
debug('All subscriptions removed, exiting pub/sub mode');
} else {
self.pub_sub_mode = true;
type = type === 'unsubscribe' ? 'subscribe' : 'psubscribe'; // Make types consistent
for (; i < command_obj.args.length; i++) {
delete self.subscription_set[type + '_' + command_obj.args[i]];
}
if (reply[2] === 0) { // No channels left that this client is subscribed to
var running_command;
i = 0;
// This should be a rare case and therefor handling it this way should be good performance wise for the general case
while (running_command = self.command_queue.get(i++)) {
if (
running_command.command === 'subscribe' ||
running_command.command === 'psubscribe' ||
running_command.command === 'unsubscribe' ||
running_command.command === 'punsubscribe'
) {
self.pub_sub_mode = i;
return;
}
// Subscribe commands take an optional callback and also emit an event, but only the first response is included in the callback
// TODO - document this or fix it so it works in a more obvious way
if (command_obj && typeof command_obj.callback === 'function') {
command_obj.callback(null, reply[1]);
}
self.emit(type, reply[1], reply[2]); // channcel, count
} else {
self.emit('error', new Error('subscriptions are active but got unknown reply type ' + type));
self.pub_sub_mode = 0;
}
} else if (!self.closing) {
self.emit('error', new Error('subscriptions are active but got an invalid reply: ' + reply));
}
}
RedisClient.prototype.return_reply = function (reply) {
var command_obj, type, queue_len;
// If the 'reply' here is actually a message received asynchronously due to a
// pubsub subscription, don't pop the command queue as we'll only be consuming
// the head command prematurely.
if (this.pub_sub_mode && reply instanceof Array && reply[0]) {
type = reply[0].toString();
function subscribe_unsubscribe (self, reply, type, subscribe) {
// Subscribe commands take an optional callback and also emit an event, but only the _last_ response is included in the callback
var command_obj = self.command_queue.get(0);
var buffer = self.options.return_buffers || self.options.detect_buffers && command_obj && command_obj.buffer_args || reply[1] === null;
var channel = buffer ? reply[1] : reply[1].toString();
var count = reply[2];
debug('Subscribe / unsubscribe command');
// Emit first, then return the callback
if (channel !== null) { // Do not emit something if there was no channel to unsubscribe from
self.emit(type, channel, count);
}
// The pub sub commands return each argument in a separate return value and have to be handled that way
if (command_obj.sub_commands_left <= 1) {
if (count !== 0 && !subscribe && command_obj.args.length === 0) {
command_obj.sub_commands_left = count;
return;
}
self.command_queue.shift();
set_subscribe(self, type, command_obj, subscribe, reply);
if (typeof command_obj.callback === 'function') {
// TODO: The current return value is pretty useless.
// Evaluate to change this in v.3 to return all subscribed / unsubscribed channels in an array including the number of channels subscribed too
command_obj.callback(null, channel);
}
if (this.pub_sub_mode && (type === 'message' || type === 'pmessage')) {
debug('Received pubsub message');
} else {
command_obj = this.command_queue.shift();
command_obj.sub_commands_left--;
}
}
queue_len = this.command_queue.length;
this.emit_idle();
function return_pub_sub (self, reply) {
var type = reply[0].toString();
if (type === 'message') { // channel, message
// TODO: Implement message_buffer
// if (self.buffers) {
// self.emit('message_buffer', reply[1], reply[2]);
// }
if (!self.options.return_buffers) { // backwards compatible. Refactor this in v.3 to always return a string on the normal emitter
self.emit('message', reply[1].toString(), reply[2].toString());
} else {
self.emit('message', reply[1], reply[2]);
}
} else if (type === 'pmessage') { // pattern, channel, message
// if (self.buffers) {
// self.emit('pmessage_buffer', reply[1], reply[2], reply[3]);
// }
if (!self.options.return_buffers) { // backwards compatible. Refactor this in v.3 to always return a string on the normal emitter
self.emit('pmessage', reply[1].toString(), reply[2].toString(), reply[3].toString());
} else {
self.emit('pmessage', reply[1], reply[2], reply[3]);
}
} else if (type === 'subscribe' || type === 'psubscribe') {
subscribe_unsubscribe(self, reply, type, true);
} else if (type === 'unsubscribe' || type === 'punsubscribe') {
subscribe_unsubscribe(self, reply, type, false);
} else {
normal_reply(self, reply);
}
}
if (command_obj && !command_obj.sub_command) {
normal_reply(this, reply, command_obj);
} else if (this.pub_sub_mode || command_obj && command_obj.sub_command) {
return_pub_sub(this, reply, command_obj);
RedisClient.prototype.return_reply = function (reply) {
if (this.pub_sub_mode === 1 && reply instanceof Array && reply.length !== 0 && reply[0]) {
return_pub_sub(this, reply);
} else {
if (this.pub_sub_mode !== 0 && this.pub_sub_mode !== 1) {
this.pub_sub_mode--;
}
/* istanbul ignore else: this is a safety check that we should not be able to trigger */
else if (!this.monitoring) {
queue_state_error(this, command_obj);
normal_reply(this, reply);
}
};
@ -731,16 +764,15 @@ RedisClient.prototype.send_command = function (command, args, callback) {
var command_str = '';
var len = 0;
var big_data = false;
var buffer_args = false;
if (process.domain && callback) {
callback = process.domain.bind(callback);
}
var command_obj = new Command(command, args, callback);
if (this.ready === false || this.stream.writable === false) {
// Handle offline commands right away
handle_offline_command(this, command_obj);
handle_offline_command(this, new OfflineCommand(command, args, callback));
return false; // Indicate buffering
}
@ -776,7 +808,7 @@ RedisClient.prototype.send_command = function (command, args, callback) {
args_copy[i] = 'null'; // Backwards compatible :/
} else if (Buffer.isBuffer(args[i])) {
args_copy[i] = args[i];
command_obj.buffer_args = true;
buffer_args = true;
big_data = true;
if (this.pipeline !== 0) {
this.pipeline += 2;
@ -803,9 +835,15 @@ RedisClient.prototype.send_command = function (command, args, callback) {
}
}
args = null;
var command_obj = new Command(command, args_copy, callback);
command_obj.buffer_args = buffer_args;
if (command === 'subscribe' || command === 'psubscribe' || command === 'unsubscribe' || command === 'punsubscribe') {
this.pub_sub_command(command_obj); // TODO: This has to be moved to the result handler
// If pub sub is already activated, keep it that way, otherwise set the number of commands to resolve until pub sub mode activates
// Deactivation of the pub sub mode happens in the result handler
if (!this.pub_sub_mode) {
this.pub_sub_mode = this.command_queue.length + 1;
}
} else if (command === 'quit') {
this.closing = true;
}
@ -886,38 +924,6 @@ RedisClient.prototype.write = function (data) {
return;
};
RedisClient.prototype.pub_sub_command = function (command_obj) {
var i, key, command, args;
if (this.pub_sub_mode === false) {
debug('Entering pub/sub mode from ' + command_obj.command);
}
this.pub_sub_mode = true;
command_obj.sub_command = true;
command = command_obj.command;
args = command_obj.args;
if (command === 'subscribe' || command === 'psubscribe') {
if (command === 'subscribe') {
key = 'sub';
} else {
key = 'psub';
}
for (i = 0; i < args.length; i++) {
this.subscription_set[key + ' ' + args[i]] = true;
}
} else {
if (command === 'unsubscribe') {
key = 'sub';
} else {
key = 'psub';
}
for (i = 0; i < args.length; i++) {
delete this.subscription_set[key + ' ' + args[i]];
}
}
};
RedisClient.prototype.end = function (flush) {
// Flush queue if wanted
if (flush) {

14
lib/command.js

@ -4,9 +4,19 @@
// a named constructor helps it show up meaningfully in the V8 CPU profiler and in heap snapshots.
function Command(command, args, callback) {
this.command = command;
this.args = args;
this.args = args; // We only need the args for the offline commands => move them into another class. We need the number of args though for pub sub
this.buffer_args = false;
this.callback = callback;
this.sub_commands_left = args.length;
}
function OfflineCommand(command, args, callback) {
this.command = command;
this.args = args;
this.callback = callback;
}
module.exports = Command;
module.exports = {
Command: Command,
OfflineCommand: OfflineCommand
};

26
test/auth.spec.js

@ -255,6 +255,32 @@ describe("client authentication", function () {
done();
});
});
it('pubsub working with auth', function (done) {
if (helper.redisProcess().spawnFailed()) this.skip();
var args = config.configureClient(parser, ip, {
password: auth
});
client = redis.createClient.apply(redis.createClient, args);
client.set('foo', 'bar');
client.subscribe('somechannel', 'another channel', function (err, res) {
client.once('ready', function () {
assert.strictEqual(client.pub_sub_mode, 1);
client.get('foo', function (err, res) {
assert.strictEqual(err.message, 'ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / QUIT allowed in this context');
done();
});
});
});
client.once('ready', function () {
// Coherent behavior with all other offline commands fires commands before emitting but does not wait till they return
assert.strictEqual(client.pub_sub_mode, 2);
client.ping(function () { // Make sure all commands were properly processed already
client.stream.destroy();
});
});
});
});
});

2
test/commands/hgetall.spec.js

@ -33,7 +33,7 @@ describe("The 'hgetall' method", function () {
});
it('handles fetching keys set using an object', function (done) {
client.HMSET("msg_test", {message: "hello"}, helper.isString("OK"));
client.HMSET("msg_test", { message: "hello" }, helper.isString("OK"));
client.hgetall("msg_test", function (err, obj) {
assert.strictEqual(1, Object.keys(obj).length);
assert.strictEqual(obj.message, "hello");

3
test/connection.spec.js

@ -230,6 +230,9 @@ describe("connection tests", function () {
});
client.on('error', function(err) {
if (err.code === 'ENETUNREACH') { // The test is run without a internet connection. Pretent it works
return done();
}
assert(/Redis connection in broken state: connection timeout.*?exceeded./.test(err.message));
// The code execution on windows is very slow at times
var add = process.platform !== 'win32' ? 25 : 125;

75
test/node_redis.spec.js

@ -303,12 +303,7 @@ describe("The node_redis client", function () {
});
});
// TODO: we should only have a single subscription in this this
// test but unsubscribing from the single channel indicates
// that one subscriber still exists, let's dig into this.
describe("and it's subscribed to a channel", function () {
// reconnect_select_db_after_pubsub
// Does not pass.
// "Connection in subscriber mode, only subscriber commands may be used"
it("reconnects, unsubscribes, and can retrieve the pre-existing data", function (done) {
client.on("ready", function on_connect() {
@ -316,6 +311,28 @@ describe("The node_redis client", function () {
client.on('unsubscribe', function (channel, count) {
// we should now be out of subscriber mode.
assert.strictEqual(channel, "recon channel");
assert.strictEqual(count, 0);
client.set('foo', 'bar', helper.isString('OK', done));
});
});
client.set("recon 1", "one");
client.subscribe("recon channel", function (err, res) {
// Do not do this in normal programs. This is to simulate the server closing on us.
// For orderly shutdown in normal programs, do client.quit()
client.stream.destroy();
});
});
it("reconnects, unsubscribes, and can retrieve the pre-existing data of a explicit channel", function (done) {
client.on("ready", function on_connect() {
client.unsubscribe('recon channel', helper.isNotError());
client.on('unsubscribe', function (channel, count) {
// we should now be out of subscriber mode.
assert.strictEqual(channel, "recon channel");
assert.strictEqual(count, 0);
client.set('foo', 'bar', helper.isString('OK', done));
});
});
@ -452,6 +469,54 @@ describe("The node_redis client", function () {
client.mget("hello", 'world');
});
});
it('monitors works in combination with the pub sub mode and the offline queue', function (done) {
var responses = [];
var pub = redis.createClient();
pub.on('ready', function () {
client.MONITOR(function (err, res) {
assert.strictEqual(res, 'OK');
pub.get('foo', helper.isNull());
});
client.subscribe('/foo', '/bar');
client.unsubscribe('/bar');
setTimeout(function () {
client.stream.destroy();
client.once('ready', function () {
pub.publish('/foo', 'hello world');
});
client.set('foo', 'bar', helper.isError());
client.subscribe('baz');
client.unsubscribe('baz');
}, 150);
var called = false;
client.on("monitor", function (time, args, rawOutput) {
responses.push(args);
assert(utils.monitor_regex.test(rawOutput), rawOutput);
if (responses.length === 7) {
assert.deepEqual(responses[0], ['subscribe', '/foo', '/bar']);
assert.deepEqual(responses[1], ['unsubscribe', '/bar']);
assert.deepEqual(responses[2], ['get', 'foo']);
assert.deepEqual(responses[3], ['subscribe', '/foo']);
assert.deepEqual(responses[4], ['subscribe', 'baz']);
assert.deepEqual(responses[5], ['unsubscribe', 'baz']);
assert.deepEqual(responses[6], ['publish', '/foo', 'hello world']);
// The publish is called right after the reconnect and the monitor is called before the message is emitted.
// Therefor we have to wait till the next tick
process.nextTick(function () {
assert(called);
client.quit(done);
pub.end(false);
});
}
});
client.on('message', function (channel, msg) {
assert.strictEqual(channel, '/foo');
assert.strictEqual(msg, 'hello world');
called = true;
});
});
});
});
describe('idle', function () {

262
test/pubsub.spec.js

@ -85,6 +85,29 @@ describe("publish/subscribe", function () {
sub.subscribe(channel, channel2);
});
it('fires a subscribe event for each channel as buffer subscribed to even after reconnecting', function (done) {
var a = false;
sub.end(true);
sub = redis.createClient({
detect_buffers: true
});
sub.on("subscribe", function (chnl, count) {
if (chnl.inspect() === new Buffer([0xAA, 0xBB, 0x00, 0xF0]).inspect()) {
assert.equal(1, count);
if (a) {
return done();
}
sub.stream.destroy();
}
});
sub.on('reconnecting', function() {
a = true;
});
sub.subscribe(new Buffer([0xAA, 0xBB, 0x00, 0xF0]), channel2);
});
it('receives messages on subscribed channel', function (done) {
var end = helper.callFuncAfter(done, 2);
sub.on("subscribe", function (chnl, count) {
@ -199,6 +222,216 @@ describe("publish/subscribe", function () {
});
});
describe("multiple subscribe / unsubscribe commands", function () {
it("reconnects properly with pub sub and select command", function (done) {
var end = helper.callFuncAfter(done, 2);
sub.select(3);
sub.set('foo', 'bar');
sub.subscribe('somechannel', 'another channel', function (err, res) {
end();
sub.stream.destroy();
});
assert(sub.ready);
sub.on('ready', function () {
sub.unsubscribe();
sub.del('foo');
sub.info(end);
});
});
it("should not go into pubsub mode with unsubscribe commands", function (done) {
sub.on('unsubscribe', function (msg) {
// The unsubscribe should not be triggered, as there was no corresponding channel
throw new Error('Test failed');
});
sub.set('foo', 'bar');
sub.unsubscribe(function (err, res) {
assert.strictEqual(res, null);
});
sub.del('foo', done);
});
it("handles multiple channels with the same channel name properly, even with buffers", function (done) {
var channels = ['a', 'b', 'a', new Buffer('a'), 'c', 'b'];
var subscribed_channels = [1, 2, 2, 2, 3, 3];
var i = 0;
sub.subscribe(channels);
sub.on('subscribe', function (channel, count) {
if (Buffer.isBuffer(channel)) {
assert.strictEqual(channel.inspect(), new Buffer(channels[i]).inspect());
} else {
assert.strictEqual(channel, channels[i].toString());
}
assert.strictEqual(count, subscribed_channels[i]);
i++;
});
sub.unsubscribe('a', 'c', 'b');
sub.get('foo', done);
});
it('should only resubscribe to channels not unsubscribed earlier on a reconnect', function (done) {
sub.subscribe('/foo', '/bar');
sub.unsubscribe('/bar', function () {
pub.pubsub('channels', function (err, res) {
assert.deepEqual(res, ['/foo']);
sub.stream.destroy();
sub.once('ready', function () {
pub.pubsub('channels', function (err, res) {
assert.deepEqual(res, ['/foo']);
sub.unsubscribe('/foo', done);
});
});
});
});
});
it("unsubscribes, subscribes, unsubscribes... single and multiple entries mixed. Withouth callbacks", function (done) {
function subscribe(channels) {
sub.unsubscribe(helper.isNull);
sub.subscribe(channels, helper.isNull);
}
var all = false;
var subscribeMsg = ['1', '3', '2', '5', 'test', 'bla'];
sub.on('subscribe', function(msg, count) {
subscribeMsg.splice(subscribeMsg.indexOf(msg), 1);
if (subscribeMsg.length === 0 && all) {
assert.strictEqual(count, 3);
done();
}
});
var unsubscribeMsg = ['1', '3', '2'];
sub.on('unsubscribe', function(msg, count) {
unsubscribeMsg.splice(unsubscribeMsg.indexOf(msg), 1);
if (unsubscribeMsg.length === 0) {
assert.strictEqual(count, 0);
all = true;
}
});
subscribe(['1', '3']);
subscribe(['2']);
subscribe(['5', 'test', 'bla']);
});
it("unsubscribes, subscribes, unsubscribes... single and multiple entries mixed. Without callbacks", function (done) {
function subscribe(channels) {
sub.unsubscribe();
sub.subscribe(channels);
}
var all = false;
var subscribeMsg = ['1', '3', '2', '5', 'test', 'bla'];
sub.on('subscribe', function(msg, count) {
subscribeMsg.splice(subscribeMsg.indexOf(msg), 1);
if (subscribeMsg.length === 0 && all) {
assert.strictEqual(count, 3);
done();
}
});
var unsubscribeMsg = ['1', '3', '2'];
sub.on('unsubscribe', function(msg, count) {
unsubscribeMsg.splice(unsubscribeMsg.indexOf(msg), 1);
if (unsubscribeMsg.length === 0) {
assert.strictEqual(count, 0);
all = true;
}
});
subscribe(['1', '3']);
subscribe(['2']);
subscribe(['5', 'test', 'bla']);
});
it("unsubscribes, subscribes, unsubscribes... single and multiple entries mixed. Without callback and concret channels", function (done) {
function subscribe(channels) {
sub.unsubscribe(channels);
sub.unsubscribe(channels);
sub.subscribe(channels);
}
var all = false;
var subscribeMsg = ['1', '3', '2', '5', 'test', 'bla'];
sub.on('subscribe', function(msg, count) {
subscribeMsg.splice(subscribeMsg.indexOf(msg), 1);
if (subscribeMsg.length === 0 && all) {
assert.strictEqual(count, 6);
done();
}
});
var unsubscribeMsg = ['1', '3', '2', '5', 'test', 'bla'];
sub.on('unsubscribe', function(msg, count) {
var pos = unsubscribeMsg.indexOf(msg);
if (pos !== -1)
unsubscribeMsg.splice(pos, 1);
if (unsubscribeMsg.length === 0) {
all = true;
}
});
subscribe(['1', '3']);
subscribe(['2']);
subscribe(['5', 'test', 'bla']);
});
it("unsubscribes, subscribes, unsubscribes... with pattern matching", function (done) {
function subscribe(channels, callback) {
sub.punsubscribe('prefix:*', helper.isNull);
sub.psubscribe(channels, function (err, res) {
helper.isNull(err);
if (callback) callback(err, res);
});
}
var all = false;
var end = helper.callFuncAfter(done, 8);
var subscribeMsg = ['prefix:*', 'prefix:3', 'prefix:2', '5', 'test:a', 'bla'];
sub.on('psubscribe', function(msg, count) {
subscribeMsg.splice(subscribeMsg.indexOf(msg), 1);
if (subscribeMsg.length === 0) {
assert.strictEqual(count, 5);
all = true;
}
});
var rest = 1;
var unsubscribeMsg = ['prefix:*', 'prefix:*', 'prefix:*', '*'];
sub.on('punsubscribe', function(msg, count) {
unsubscribeMsg.splice(unsubscribeMsg.indexOf(msg), 1);
if (all) {
assert.strictEqual(unsubscribeMsg.length, 0);
assert.strictEqual(count, rest--); // Print the remaining channels
end();
} else {
assert.strictEqual(msg, 'prefix:*');
assert.strictEqual(count, rest++ - 1);
}
});
sub.on('pmessage', function (pattern, channel, msg) {
assert.strictEqual(msg, 'test');
assert.strictEqual(pattern, 'prefix:*');
assert.strictEqual(channel, 'prefix:1');
end();
});
subscribe(['prefix:*', 'prefix:3'], function () {
pub.publish('prefix:1', new Buffer('test'), function () {
subscribe(['prefix:2']);
subscribe(['5', 'test:a', 'bla'], function () {
assert(all);
});
sub.punsubscribe(function (err, res) {
assert(!err);
assert.strictEqual(res, 'bla');
assert(all);
all = false; // Make sure the callback is actually after the emit
end();
});
sub.pubsub('channels', function (err, res) {
assert.strictEqual(res.length, 0);
end();
});
});
});
});
});
describe('unsubscribe', function () {
it('fires an unsubscribe event', function (done) {
sub.on("subscribe", function (chnl, count) {
@ -237,24 +470,29 @@ describe("publish/subscribe", function () {
it('executes callback when unsubscribe is called and there are no subscriptions', function (done) {
pub.unsubscribe(function (err, results) {
assert.strictEqual(null, results);
return done(err);
done(err);
});
});
});
describe('psubscribe', function () {
// test motivated by issue #753
it('allows all channels to be subscribed to using a * pattern', function (done) {
sub.end(false);
sub = redis.createClient({
return_buffers: true
});
sub.on('ready', function () {
sub.psubscribe('*');
sub.on("pmessage", function(pattern, channel, message) {
assert.strictEqual(pattern, '*');
assert.strictEqual(channel, '/foo');
assert.strictEqual(message, 'hello world');
return done();
assert.strictEqual(pattern.inspect(), new Buffer('*').inspect());
assert.strictEqual(channel.inspect(), new Buffer('/foo').inspect());
assert.strictEqual(message.inspect(), new Buffer('hello world').inspect());
done();
});
pub.publish('/foo', 'hello world');
});
});
});
describe('punsubscribe', function () {
it('does not complain when punsubscribe is called and there are no subscriptions', function () {
@ -264,7 +502,7 @@ describe("publish/subscribe", function () {
it('executes callback when punsubscribe is called and there are no subscriptions', function (done) {
pub.punsubscribe(function (err, results) {
assert.strictEqual(null, results);
return done(err);
done(err);
});
});
});
@ -330,20 +568,16 @@ describe("publish/subscribe", function () {
}, 40);
});
// TODO: Fix pub sub
// And there's more than just those two issues
describe.skip('FIXME: broken pub sub', function () {
it("should not publish a message without any publish command", function (done) {
pub.set('foo', 'message');
pub.set('bar', 'hello');
pub.mget('foo', 'bar');
pub.subscribe('channel');
pub.subscribe('channel', function () {
setTimeout(done, 50);
});
pub.on('message', function (msg) {
done(new Error('This message should not have been published: ' + msg));
});
setTimeout(done, 200);
});
});
afterEach(function () {

Loading…
Cancel
Save