You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-09 00:22:08 +03:00
Merge pull request #1017 from NodeRedis/pubsub
Fix pub sub mode Fixes #603 Fixes #577 Fixes #137
This commit is contained in:
38
README.md
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)
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
244
index.js
244
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();
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
}
|
||||
self.pub_sub_mode = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
// 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));
|
||||
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;
|
||||
}
|
||||
} else if (!self.closing) {
|
||||
self.emit('error', new Error('subscriptions are active but got an invalid reply: ' + reply));
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
command_obj.sub_commands_left--;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
if (this.pub_sub_mode && (type === 'message' || type === 'pmessage')) {
|
||||
debug('Received pubsub message');
|
||||
if (this.pub_sub_mode === 1 && reply instanceof Array && reply.length !== 0 && reply[0]) {
|
||||
return_pub_sub(this, reply);
|
||||
} else {
|
||||
command_obj = this.command_queue.shift();
|
||||
}
|
||||
|
||||
queue_len = this.command_queue.length;
|
||||
|
||||
this.emit_idle();
|
||||
|
||||
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);
|
||||
}
|
||||
/* 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);
|
||||
if (this.pub_sub_mode !== 0 && this.pub_sub_mode !== 1) {
|
||||
this.pub_sub_mode--;
|
||||
}
|
||||
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) {
|
||||
|
@@ -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
|
||||
};
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -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");
|
||||
|
@@ -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;
|
||||
|
@@ -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 () {
|
||||
|
@@ -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,22 +470,27 @@ 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.psubscribe('*');
|
||||
sub.on("pmessage", function(pattern, channel, message) {
|
||||
assert.strictEqual(pattern, '*');
|
||||
assert.strictEqual(channel, '/foo');
|
||||
assert.strictEqual(message, 'hello world');
|
||||
return 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.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');
|
||||
});
|
||||
pub.publish('/foo', 'hello world');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,19 +568,15 @@ 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.on('message', function (msg) {
|
||||
done(new Error('This message should not have been published: ' + msg));
|
||||
});
|
||||
setTimeout(done, 200);
|
||||
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', function () {
|
||||
setTimeout(done, 50);
|
||||
});
|
||||
pub.on('message', function (msg) {
|
||||
done(new Error('This message should not have been published: ' + msg));
|
||||
});
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user