1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-06 02:15:48 +03:00

Lots of bugs fixed.

*  connection error did not properly trigger reconnection logic [GH-85]
*  client.hmget(key, [val1, val2]) was not expanding properly [GH-66]
*  client.quit() while in pub/sub mode would throw an error [GH-87]
*  client.multi(['hmset', 'key', {foo: 'bar'}]) fails [GH-92]
This commit is contained in:
Matt Ranney
2011-04-21 16:48:14 -10:00
parent f624fa6234
commit f10ff9e916
11 changed files with 161 additions and 59 deletions

View File

@@ -537,6 +537,7 @@ In order of first contribution, they are:
* [Aivo Paas](http://github.com/aivopaas) * [Aivo Paas](http://github.com/aivopaas)
* [Paul Carey](https://github.com/paulcarey) * [Paul Carey](https://github.com/paulcarey)
* [Pieter Noordhuis](https://github.com/pietern) * [Pieter Noordhuis](https://github.com/pietern)
* [Vladimir Dronnikov](https://github.com/dvv)
Thanks. Thanks.

View File

@@ -1,6 +1,15 @@
Changelog Changelog
========= =========
## v0.6.0 - April 21, 2011
Lots of bugs fixed.
* connection error did not properly trigger reconnection logic [GH-85]
* client.hmget(key, [val1, val2]) was not expanding properly [GH-66]
* client.quit() while in pub/sub mode would throw an error [GH-87]
* client.multi(['hmset', 'key', {foo: 'bar'}]) fails [GH-92]
## v0.5.11 - April 7, 2011 ## v0.5.11 - April 7, 2011
Added DISCARD Added DISCARD

5
examples/mget.js Normal file
View File

@@ -0,0 +1,5 @@
var client = require("redis").createClient();
client.mget(["sessions started", "sessions started", "foo"], function (err, res) {
console.dir(res);
});

19
examples/subquery.js Normal file
View File

@@ -0,0 +1,19 @@
var client = require("redis").createClient();
function print_results(obj) {
console.dir(obj);
}
// build a map of all keys and their types
client.keys("*", function (err, all_keys) {
var key_types = {};
all_keys.forEach(function (key, pos) { // use second arg of forEach to get pos
client.type(key, function (err, type) {
key_types[key] = type;
if (pos === all_keys.length - 1) { // callbacks all run in order
print_results(key_types);
}
});
});
});

View File

@@ -3,6 +3,7 @@
var net = require("net"), var net = require("net"),
util = require("./lib/util").util, util = require("./lib/util").util,
Queue = require("./lib/queue").Queue, Queue = require("./lib/queue").Queue,
to_array = require("./lib/to_array"),
events = require("events"), events = require("events"),
parsers = [], parsers = [],
default_port = 6379, default_port = 6379,
@@ -23,17 +24,6 @@ try {
parsers.push(require("./lib/parser/javascript")); parsers.push(require("./lib/parser/javascript"));
function to_array(args) {
var len = args.length,
arr = new Array(len), i;
for (i = 0; i < len; i += 1) {
arr[i] = args[i];
}
return arr;
}
function RedisClient(stream, options) { function RedisClient(stream, options) {
this.stream = stream; this.stream = stream;
this.options = options || {}; this.options = options || {};
@@ -45,8 +35,9 @@ function RedisClient(stream, options) {
this.command_queue = new Queue(); // holds sent commands to de-pipeline them this.command_queue = new Queue(); // holds sent commands to de-pipeline them
this.offline_queue = new Queue(); // holds commands issued but not able to be sent this.offline_queue = new Queue(); // holds commands issued but not able to be sent
this.commands_sent = 0; this.commands_sent = 0;
this.retry_delay = 250; this.retry_delay = 250; // inital reconnection delay
this.retry_backoff = 1.7; this.current_retry_delay = this.retry_delay;
this.retry_backoff = 1.7; // each retry waits current delay * retry_backoff
this.subscriptions = false; this.subscriptions = false;
this.monitoring = false; this.monitoring = false;
this.closing = false; this.closing = false;
@@ -79,7 +70,7 @@ function RedisClient(stream, options) {
return_buffers: self.options.return_buffers || false return_buffers: self.options.return_buffers || false
}); });
// "reply error" is an error sent back by redis // "reply error" is an error sent back by Redis
this.reply_parser.on("reply error", function (reply) { this.reply_parser.on("reply error", function (reply) {
self.return_error(new Error(reply)); self.return_error(new Error(reply));
}); });
@@ -103,7 +94,7 @@ function RedisClient(stream, options) {
if (this.closing) { if (this.closing) {
return; return;
} }
var message = "Redis connection to " + self.host + ":" + self.port + " failed - " + msg.message; var message = "Redis connection to " + self.host + ":" + self.port + " failed - " + msg.message;
if (exports.debug_mode) { if (exports.debug_mode) {
@@ -122,10 +113,14 @@ function RedisClient(stream, options) {
} }
}); });
self.command_queue = new Queue(); self.command_queue = new Queue();
self.connected = false; self.connected = false;
self.ready = false; self.ready = false;
self.emit("error", new Error(message)); self.emit("error", new Error(message));
// "error" events get turned into exceptions if they aren't listened for. If the user handled this error
// then we should try to reconnect.
self.connection_gone("error");
}); });
this.stream.on("close", function () { this.stream.on("close", function () {
@@ -153,11 +148,12 @@ RedisClient.prototype.on_connect = function () {
this.connected = true; this.connected = true;
this.ready = false; this.ready = false;
this.attempts = 0;
this.connections += 1; this.connections += 1;
this.command_queue = new Queue(); this.command_queue = new Queue();
this.emitted_end = false; this.emitted_end = false;
this.retry_timer = null; this.retry_timer = null;
this.retry_delay = 250; this.current_retry_delay = this.retry_time;
this.stream.setNoDelay(); this.stream.setNoDelay();
this.stream.setTimeout(0); this.stream.setTimeout(0);
@@ -225,7 +221,7 @@ RedisClient.prototype.ready_check = function () {
// expose info key/vals to users // expose info key/vals to users
self.server_info = obj; self.server_info = obj;
if (!obj["loading"] || (obj["loading"] && obj["loading"] == 0)) { if (!obj.loading || (obj.loading && obj.loading === "0")) {
if (exports.debug_mode) { if (exports.debug_mode) {
console.log("Redis server ready."); console.log("Redis server ready.");
} }
@@ -267,56 +263,57 @@ RedisClient.prototype.connection_gone = function (why) {
var self = this; var self = this;
// If a retry is already in progress, just let that happen // If a retry is already in progress, just let that happen
if (self.retry_timer) { if (this.retry_timer) {
return; return;
} }
// Note that this may trigger another "close" or "end" event // Note that this may trigger another "close" or "end" event
self.stream.destroy(); this.stream.destroy();
if (exports.debug_mode) { if (exports.debug_mode) {
console.warn("Redis connection is gone from " + why + " event."); console.warn("Redis connection is gone from " + why + " event.");
} }
self.connected = false; this.connected = false;
self.ready = false; this.ready = false;
self.subscriptions = false; this.subscriptions = false;
self.monitoring = false; this.monitoring = false;
// since we are collapsing end and close, users don't expect to be called twice // since we are collapsing end and close, users don't expect to be called twice
if (! self.emitted_end) { if (! this.emitted_end) {
self.emit("end"); this.emit("end");
self.emitted_end = true; this.emitted_end = true;
} }
self.command_queue.forEach(function (args) { this.command_queue.forEach(function (args) {
if (typeof args[2] === "function") { if (typeof args[2] === "function") {
args[2]("Server connection closed"); args[2]("Server connection closed");
} }
}); });
self.command_queue = new Queue(); this.command_queue = new Queue();
// If this is a requested shutdown, then don't retry // If this is a requested shutdown, then don't retry
if (self.closing) { if (this.closing) {
self.retry_timer = null; this.retry_timer = null;
return; return;
} }
this.current_retry_delay = this.retry_delay * this.retry_backoff;
if (exports.debug_mode) { if (exports.debug_mode) {
console.log("Retry connection in " + self.retry_delay + " ms"); console.log("Retry connection in " + this.current_retry_delay + " ms");
} }
self.attempts += 1; this.attempts += 1;
self.emit("reconnecting", { this.emit("reconnecting", {
delay: self.retry_delay, delay: this.current_retry_delay,
attempt: self.attempts attempt: this.attempts
}); });
self.retry_timer = setTimeout(function () { this.retry_timer = setTimeout(function () {
if (exports.debug_mode) { if (exports.debug_mode) {
console.log("Retrying connection..."); console.log("Retrying connection...");
} }
self.retry_delay = self.retry_delay * self.retry_backoff;
self.stream.connect(self.port, self.host); self.stream.connect(self.port, self.host);
self.retry_timer = null; self.retry_timer = null;
}, self.retry_delay); }, this.current_retry_delay);
}; };
RedisClient.prototype.on_data = function (data) { RedisClient.prototype.on_data = function (data) {
@@ -369,7 +366,7 @@ RedisClient.prototype.return_reply = function (reply) {
this.emit("idle"); this.emit("idle");
this.command_queue = new Queue(); this.command_queue = new Queue();
} }
if (command_obj && !command_obj.sub_command) { if (command_obj && !command_obj.sub_command) {
if (typeof command_obj.callback === "function") { if (typeof command_obj.callback === "function") {
// HGETALL special case replies with keyed Buffers // HGETALL special case replies with keyed Buffers
@@ -394,7 +391,7 @@ RedisClient.prototype.return_reply = function (reply) {
} else if (exports.debug_mode) { } else if (exports.debug_mode) {
console.log("no callback for reply: " + (reply && reply.toString && reply.toString())); console.log("no callback for reply: " + (reply && reply.toString && reply.toString()));
} }
} else if (this.subscriptions) { } else if (this.subscriptions || command_obj.sub_command) {
if (Array.isArray(reply)) { if (Array.isArray(reply)) {
type = reply[0].toString(); type = reply[0].toString();
@@ -413,7 +410,7 @@ RedisClient.prototype.return_reply = function (reply) {
} else { } else {
throw new Error("subscriptions are active but got unknown reply type " + type); throw new Error("subscriptions are active but got unknown reply type " + type);
} }
} else { } else if (! this.closing) {
throw new Error("subscriptions are active but got an invalid reply: " + reply); throw new Error("subscriptions are active but got an invalid reply: " + reply);
} }
} else if (this.monitoring) { } else if (this.monitoring) {
@@ -457,6 +454,10 @@ RedisClient.prototype.send_command = function () {
} }
} }
if (args.length === 2 && Array.isArray(args[1])) {
args = [args[0]].concat(args[1]);
}
command_obj = { command_obj = {
command: command, command: command,
args: args, args: args,
@@ -573,7 +574,8 @@ function Multi(client, args) {
// string commands // string commands
"get", "set", "setnx", "setex", "append", "substr", "strlen", "del", "exists", "incr", "decr", "mget", "get", "set", "setnx", "setex", "append", "substr", "strlen", "del", "exists", "incr", "decr", "mget",
// list commands // list commands
"rpush", "lpush", "rpushx", "lpushx", "linsert", "rpop", "lpop", "brpop", "blpop", "llen", "lindex", "lset", "lrange", "ltrim", "lrem", "rpoplpush", "rpush", "lpush", "rpushx", "lpushx", "linsert", "rpop", "lpop", "brpop", "blpop", "brpoplpush", "llen", "lindex", "lset", "lrange",
"ltrim", "lrem", "rpoplpush",
// set commands // set commands
"sadd", "srem", "smove", "sismember", "scard", "spop", "srandmember", "sinter", "sinterstore", "sunion", "sunionstore", "sdiff", "sdiffstore", "smembers", "sadd", "srem", "smove", "sismember", "scard", "spop", "srandmember", "sinter", "sinterstore", "sunion", "sunionstore", "sdiff", "sdiffstore", "smembers",
// sorted set commands // sorted set commands
@@ -668,7 +670,7 @@ Multi.prototype.exec = function (callback) {
// drain queue, callback will catch "QUEUED" or error // drain queue, callback will catch "QUEUED" or error
// Can't use a for loop here, as we need closure around the index. // Can't use a for loop here, as we need closure around the index.
this.queue.forEach(function (args, index) { this.queue.forEach(function (args, index) {
var command = args[0]; var command = args[0], obj;
if (typeof args[args.length - 1] === "function") { if (typeof args[args.length - 1] === "function") {
args = args.slice(1, -1); args = args.slice(1, -1);
} else { } else {
@@ -677,6 +679,13 @@ Multi.prototype.exec = function (callback) {
if (args.length === 1 && Array.isArray(args[0])) { if (args.length === 1 && Array.isArray(args[0])) {
args = args[0]; args = args[0];
} }
if (command === 'hmset' && typeof args[1] === 'object') {
obj = args.pop();
Object.keys(obj).forEach(function (key) {
args.push(key);
args.push(obj[key]);
});
}
this.client.send_command(command, args, function (err, reply) { this.client.send_command(command, args, function (err, reply) {
if (err) { if (err) {
var cur = self.queue[index]; var cur = self.queue[index];

View File

@@ -1,22 +1,13 @@
function to_array(args) { var to_array = require("./to_array");
var len = args.length,
arr = new Array(len), i;
for (i = 0; i < len; i += 1) {
arr[i] = args[i];
}
return arr;
}
// Queue class adapted from Tim Caswell's pattern library // Queue class adapted from Tim Caswell's pattern library
// http://github.com/creationix/pattern/blob/master/lib/pattern/queue.js // http://github.com/creationix/pattern/blob/master/lib/pattern/queue.js
var Queue = function () { function Queue() {
this.tail = []; this.tail = [];
this.head = to_array(arguments); this.head = to_array(arguments);
this.offset = 0; this.offset = 0;
}; }
Queue.prototype.shift = function () { Queue.prototype.shift = function () {
if (this.offset === this.head.length) { if (this.offset === this.head.length) {

13
lib/to_array.js Normal file
View File

@@ -0,0 +1,13 @@
// the "new Array(len)" syntax is legal and optimized by V8, but JSHint is utterly confused by it.
function to_array(args) {
var len = args.length,
arr = new Array(len), i;
for (i = 0; i < len; i += 1) {
arr[i] = args[i];
}
return arr;
};
module.exports = to_array;

View File

@@ -1,5 +1,5 @@
{ "name" : "redis", { "name" : "redis",
"version" : "0.5.11", "version" : "0.6.0",
"description" : "Redis client library", "description" : "Redis client library",
"author": "Matt Ranney <mjr@ranney.com>", "author": "Matt Ranney <mjr@ranney.com>",
"contributors": [ "contributors": [
@@ -11,7 +11,8 @@
"Aivo Paas", "Aivo Paas",
"Paul Carey", "Paul Carey",
"Pieter Noordhuis", "Pieter Noordhuis",
"Andy Ray" "Andy Ray",
"Vladimir Dronnikov"
], ],
"main": "./index.js", "main": "./index.js",
"scripts": { "scripts": {

11
test.js
View File

@@ -318,7 +318,16 @@ tests.HMGET = function () {
assert.strictEqual("abcdefghij", reply[0].toString(), name); assert.strictEqual("abcdefghij", reply[0].toString(), name);
assert.strictEqual("a type of value", reply[1].toString(), name); assert.strictEqual("a type of value", reply[1].toString(), name);
}); });
client.HMGET(key1, ["0123456789"], function (err, reply) {
assert.strictEqual("abcdefghij", reply[0], name);
});
client.HMGET(key1, ["0123456789", "some manner of key"], function (err, reply) {
assert.strictEqual("abcdefghij", reply[0], name);
assert.strictEqual("a type of value", reply[1], name);
});
client.HMGET(key1, "missing thing", "another missing thing", function (err, reply) { client.HMGET(key1, "missing thing", "another missing thing", function (err, reply) {
assert.strictEqual(null, reply[0], name); assert.strictEqual(null, reply[0], name);
assert.strictEqual(null, reply[1], name); assert.strictEqual(null, reply[1], name);

27
tests/reconnect_test.js Normal file
View File

@@ -0,0 +1,27 @@
var redis = require("redis").createClient();
redis.on("error", function (err) {
console.log("Redis says: " + err);
});
redis.on("ready", function () {
console.log("Redis ready.");
});
redis.on("reconnecting", function (arg) {
console.log("Redis reconnecting: " + JSON.stringify(arg));
});
redis.on("connect", function () {
console.log("Redis connected.");
});
setInterval(function () {
var now = Date.now();
redis.set("now", now, function (err, res) {
if (err) {
console.log(now + " Redis reply error: " + err);
} else {
console.log(now + " Redis reply: " + res);
}
});
}, 200);

18
tests/sub_quit_test.js Normal file
View File

@@ -0,0 +1,18 @@
var client = require("redis").createClient(),
client2 = require("redis").createClient();
client.subscribe("something");
client.on("subscribe", function (channel, count) {
console.log("Got sub: " + channel);
client.unsubscribe("something");
});
client.on("unsubscribe", function (channel, count) {
console.log("Got unsub: " + channel + ", quitting");
client.quit();
});
// exercise unsub before sub
client2.unsubscribe("something");
client2.subscribe("another thing");
client2.quit();