Skip to content

Commit

Permalink
Some bug fixes:
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
mranney committed Dec 30, 2010
1 parent aa3cefe commit ccce845
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 33 deletions.
40 changes: 39 additions & 1 deletion README.md
Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions 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.
Expand Down
85 changes: 64 additions & 21 deletions index.js
Expand Up @@ -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");
Expand All @@ -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();
Expand Down Expand Up @@ -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...");
Expand Down Expand Up @@ -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;
}
};

Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,5 +1,5 @@
{ "name" : "redis",
"version" : "0.4.2",
"version" : "0.5.0",
"description" : "Redis client library",
"author": "Matt Ranney <mjr@ranney.com>",
"contributors": [
Expand Down
66 changes: 56 additions & 10 deletions test.js
Expand Up @@ -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);
});
};
Expand All @@ -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"),
Expand All @@ -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);
Expand Down Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit ccce845

Please sign in to comment.