From ccce845cc28a436a031706470a09bb428533cc9f Mon Sep 17 00:00:00 2001 From: Matt Ranney Date: Wed, 29 Dec 2010 17:48:40 -0800 Subject: [PATCH] Some bug fixes: * An important bug fix in reconnection logic. Previously, reply callbacks would be invoked twice after a reconnect. * Changed error callback argument to be an actual Error object. New feature: * Add friendly syntax for HMSET using an object. --- README.md | 40 ++++++++++++++++++++++++- changelog.md | 12 ++++++++ index.js | 85 +++++++++++++++++++++++++++++++++++++++------------- package.json | 2 +- test.js | 66 +++++++++++++++++++++++++++++++++------- 5 files changed, 172 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 52cf53adaac..9ee9bd732a1 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ The performance of `node_redis` improves dramatically with pipelining. ## Usage -Simple example, included as `example.js`: +Simple example, included as `examples/simple.js`: var redis = require("redis"), client = redis.createClient(); @@ -195,6 +195,44 @@ want to do this: `client.end()` is useful for timeout cases where something is stuck or taking too long and you want to start over. +## Friendlier hash commands + +Most Redis commands take a single String or an Array of Strings as arguments, and replies are sent back as a single String or an Array of Strings. When dealing with hash values, there are a couple of useful exceptions to this. + +### client.hgetall(hash) + +The reply from an HGETALL command will be converted into a JavaScript Object by `node_redis`. That way you can interact +with the responses using JavaScript syntax. + +Example: + + client.hmset("hosts", "mjr", "1", "another", "23", "home", "1234"); + client.hgetall("hosts", function (err, obj) { + console.dir(obj); + }); + +Output: + + { mjr: '1', another: '23', home: '1234' } + +### client.hmset(hash, obj, [callback]) + +Multiple values in a hash can be set by supplying an object: + + client.HMSET(key2, { + "0123456789": "abcdefghij", + "some manner of key": "a type of value" + }); + +The properties and values of this Object will be set as keys and values in the Redis hash. + +### client.hmset(hash, key1, val1, ... keyn, valn, [callback]) + +Multiple values may also be set by supplying a list: + + client.HMSET(key1, "0123456789", "abcdefghij", "some manner of key", "a type of value"); + + ## Publish / Subscribe Here is a simple example of the API for publish / subscribe. This program opens two diff --git a/changelog.md b/changelog.md index de523a5828c..116af5644d3 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,18 @@ Changelog ========= +## v0.5.0 - December 29, 2010 + +Some bug fixes: + +* An important bug fix in reconnection logic. Previously, reply callbacks would be invoked twice after + a reconnect. +* Changed error callback argument to be an actual Error object. + +New feature: + +* Add friendly syntax for HMSET using an object. + ## v0.4.1 - December 8, 2010 Remove warning about missing hiredis. You probably do want it though. diff --git a/index.js b/index.js index 3c779a8a98e..dd2bbda8bca 100644 --- a/index.js +++ b/index.js @@ -77,6 +77,18 @@ function RedisClient(stream, options) { return_buffers: self.options.return_buffers || false }); + // "reply error" is an error sent back by redis + self.reply_parser.on("reply error", function (reply) { + self.return_error(new Error(reply)); + }); + self.reply_parser.on("reply", function (reply) { + self.return_reply(reply); + }); + // "error" is bad. Somehow the parser got confused. It'll try to reset and continue. + self.reply_parser.on("error", function (err) { + self.emit("error", new Error("Redis reply parser error: " + err.stack)); + }); + this.stream.on("connect", function () { if (exports.debug_mode) { console.log("Stream connected"); @@ -86,18 +98,6 @@ function RedisClient(stream, options) { self.command_queue = new Queue(); self.emitted_end = false; - // "reply error" is an error sent back by redis - self.reply_parser.on("reply error", function (reply) { - self.return_error(reply); - }); - self.reply_parser.on("reply", function (reply) { - self.return_reply(reply); - }); - // "error" is bad. Somehow the parser got confused. It'll try to reset and continue. - self.reply_parser.on("error", function (err) { - self.emit("error", new Error("Redis reply parser error: " + err.stack)); - }); - self.retry_timer = null; self.retry_delay = 250; self.stream.setNoDelay(); @@ -196,7 +196,10 @@ RedisClient.prototype.connection_gone = function (why) { console.log("Retry conneciton in " + self.retry_delay + " ms"); } self.attempts += 1; - self.emit("reconnecting", "delay " + self.retry_delay + ", attempt " + self.attempts); + self.emit("reconnecting", { + delay: self.retry_delay, + attempt: self.attempts + }); self.retry_timer = setTimeout(function () { if (exports.debug_mode) { console.log("Retrying connection..."); @@ -233,16 +236,16 @@ RedisClient.prototype.return_error = function (err) { if (command_obj && typeof command_obj.callback === "function") { try { command_obj.callback(err); - } catch (err) { + } catch (callback_err) { // if a callback throws an exception, re-throw it on a new stack so the parser can keep going process.nextTick(function () { - throw err; + throw callback_err; }); } } else { - console.log("no callback to send error: " + util.inspect(err)); + console.log("node_redis: no callback to send error: " + util.inspect(err)); // this will probably not make it anywhere useful, but we might as well throw - throw new Error(err); + throw err; } }; @@ -439,9 +442,8 @@ function Multi(client, args) { } } - -// Official source is: http://code.google.com/p/redis/wiki/CommandReference -// This list is taken from src/redis.c +// Official source is: http://redis.io/commands.json +// This list needs to be updated, and perhaps auto-updated somehow. [ // string commands "get", "set", "setnx", "setex", "append", "substr", "strlen", "del", "exists", "incr", "decr", "mget", @@ -453,7 +455,7 @@ function Multi(client, args) { "zadd", "zincrby", "zrem", "zremrangebyscore", "zremrangebyrank", "zunionstore", "zinterstore", "zrange", "zrangebyscore", "zrevrangebyscore", "zcount", "zrevrange", "zcard", "zscore", "zrank", "zrevrank", // hash commands - "hset", "hsetnx", "hget", "hmset", "hmget", "hincrby", "hdel", "hlen", "hkeys", "hgetall", "hexists", "incrby", "decrby", + "hset", "hsetnx", "hget", "hmget", "hincrby", "hdel", "hlen", "hkeys", "hgetall", "hexists", "incrby", "decrby", // misc "getset", "mset", "msetnx", "randomkey", "select", "move", "rename", "renamenx", "expire", "expireat", "keys", "dbsize", "auth", "ping", "echo", "save", "bgsave", "bgwriteaof", "shutdown", "lastsave", "type", "sync", "flushdb", "flushall", "sort", "info", @@ -476,6 +478,47 @@ function Multi(client, args) { Multi.prototype[command.toUpperCase()] = Multi.prototype[command]; }); +RedisClient.prototype.hmset = function () { + var args = to_array(arguments), tmp_args; + if (args.length >= 2 && typeof args[0] === "string" && typeof args[1] === "object") { + tmp_args = [ "hmset", args[0] ]; + Object.keys(args[1]).map(function (key) { + tmp_args.push(key); + tmp_args.push(args[1][key]); + }); + if (args[2]) { + tmp_args.push(args[2]); + } + args = tmp_args; + } else { + args.unshift("hmset"); + } + + this.send_command.apply(this, args); +}; +RedisClient.prototype.HMSET = RedisClient.prototype.hmset; + +Multi.prototype.hmset = function () { + var args = to_array(arguments), tmp_args; + if (args.length >= 2 && typeof args[0] === "string" && typeof args[1] === "object") { + tmp_args = [ "hmset", args[0] ]; + Object.keys(args[1]).map(function (key) { + tmp_args.push(key); + tmp_args.push(args[1][key]); + }); + if (args[2]) { + tmp_args.push(args[2]); + } + args = tmp_args; + } else { + args.unshift("hmset"); + } + + this.queue.push(args); + return this; +}; +Multi.prototype.HMSET = Multi.prototype.hmset; + Multi.prototype.exec = function (callback) { var self = this; diff --git a/package.json b/package.json index d69ecab3631..c5da1928b5e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { "name" : "redis", - "version" : "0.4.2", + "version" : "0.5.0", "description" : "Redis client library", "author": "Matt Ranney ", "contributors": [ diff --git a/test.js b/test.js index 7bc33497dfe..039b06afe9e 100644 --- a/test.js +++ b/test.js @@ -211,13 +211,19 @@ tests.MULTI_6 = function () { client.multi() .hmset("multihash", "a", "foo", "b", 1) + .hmset("multihash", { + extra: "fancy", + things: "here" + }) .hgetall("multihash") .exec(function (err, replies) { assert.strictEqual(null, err); assert.equal("OK", replies[0]); - assert.equal(Object.keys(replies[1]).length, 2); - assert.equal("foo", replies[1].a.toString()); - assert.equal("1", replies[1].b.toString()); + assert.equal(Object.keys(replies[2]).length, 4); + assert.equal("foo", replies[2].a); + assert.equal("1", replies[2].b); + assert.equal("fancy", replies[2].extra); + assert.equal("here", replies[2].things); next(name); }); }; @@ -237,6 +243,30 @@ tests.WATCH_MULTI = function () { } }; +tests.reconnect = function () { + var name = "reconnect"; + + client.set("recon 1", "one"); + client.set("recon 2", "two", 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(); + }); + + client.on("reconnecting", function on_recon(params) { + client.on("connect", function on_connect() { + client.select(test_db_num, require_string("OK", name)); + client.get("recon 1", require_string("one", name)); + client.get("recon 1", require_string("one", name)); + client.get("recon 2", require_string("two", name)); + client.get("recon 2", require_string("two", name)); + client.removeListener("connect", on_connect); + client.removeListener("reconnecting", on_recon); + next(name); + }); + }); +}; + tests.HSET = function () { var key = "test hash", field1 = new Buffer("0123456789"), @@ -257,17 +287,30 @@ tests.HSET = function () { client.HSET(key, field2, value2, last(name, require_number(0, name))); }; + tests.HMGET = function () { - var key = "test hash", name = "HMGET"; + var key1 = "test hash 1", key2 = "test hash 2", name = "HMGET"; + + // redis-like hmset syntax + client.HMSET(key1, "0123456789", "abcdefghij", "some manner of key", "a type of value", require_string("OK", name)); - client.HMSET(key, "0123456789", "abcdefghij", "some manner of key", "a type of value", require_string("OK", name)); + // fancy hmset syntax + client.HMSET(key2, { + "0123456789": "abcdefghij", + "some manner of key": "a type of value" + }, require_string("OK", name)); - client.HMGET(key, "0123456789", "some manner of key", function (err, reply) { + client.HMGET(key1, "0123456789", "some manner of key", function (err, reply) { + assert.strictEqual("abcdefghij", reply[0].toString(), name); + assert.strictEqual("a type of value", reply[1].toString(), name); + }); + + client.HMGET(key2, "0123456789", "some manner of key", function (err, reply) { assert.strictEqual("abcdefghij", reply[0].toString(), name); assert.strictEqual("a type of value", reply[1].toString(), name); }); - client.HMGET(key, "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[1], name); next(name); @@ -1014,7 +1057,10 @@ function run_next_test() { console.log("Using reply parser " + client.reply_parser.name); -client.on("connect", function () { +client.on("connect", function start_tests() { + // remove listener so we don't restart all tests on reconnect + client.removeListener("connect", start_tests); + // Fetch and stash info results in case anybody needs info on the server we are using. client.info(function (err, reply) { var obj = {}; @@ -1055,8 +1101,8 @@ client3.on("error", function (err) { process.exit(); }); -client.on("reconnecting", function (msg) { - console.log("reconnecting: " + msg); +client.on("reconnecting", function (params) { + console.log("reconnecting: " + util.inspect(params)); }); process.on('uncaughtException', function (err) {