Skip to content

Commit

Permalink
Merge pull request #148 from luin/reconnect-on-error
Browse files Browse the repository at this point in the history
Support reconnecting on the specified error. #144
  • Loading branch information
luin committed Sep 16, 2015
2 parents 06afbce + 804cf7f commit bdd9636
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 5 deletions.
1 change: 1 addition & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Creates a Redis instance
| [options.lazyConnect] | <code>boolean</code> | <code>false</code> | By default, When a new `Redis` instance is created, it will connect to Redis server automatically. If you want to keep disconnected util a command is called, you can pass the `lazyConnect` option to the constructor: |
| [options.keyPrefix] | <code>string</code> | <code>&quot;&#x27;&#x27;&quot;</code> | The prefix to prepend to all keys in a command. ```javascript var redis = new Redis({ lazyConnect: true }); // No attempting to connect to the Redis server here. // Now let's connect to the Redis server redis.get('foo', function () { }); ``` |
| [options.retryStrategy] | <code>function</code> | | See "Quick Start" section |
| [options.reconnectOnError] | <code>function</code> | | See "Quick Start" section |

**Example**
```js
Expand Down
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### Master Branch

* Support reconnecting on the specified error.

### v1.8.0 - September 9, 2015

* Add keepAlive option(defaults to `true`).
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,26 @@ This behavior can be disabled by setting the `autoResubscribe` option to `false`
And if the previous connection has some unfulfilled commands (most likely blocking commands such as `brpop` and `blpop`),
the client will resend them when reconnected. This behavior can be disabled by setting the `autoResendUnfulfilledCommands` option to `false`.

### Reconnect on error

Besides auto-reconnect when the connection is closed, ioredis supports reconnecting on the specified errors by the `reconnectOnError` option. Here's an example that will reconnect when receiving `READONLY` error:

```javascript
var redis = new Redis({
reconnectOnError: function (err) {
var targetError = 'READONLY';
if (err.message.slice(0, targetError.length) === targetError) {
// Only reconnect when the error starts with "READONLY"
return true; // or `return 1;`
}
}
});
```

This feature is useful when using Amazon ElastiCache. Once failover happens, Amazon ElastiCache will switch the master we currently connected with to a slave, leading to the following writes fails with the error `READONLY`. Using `reconnectOnError`, we can force the connection to reconnect on this error in order to connect to the new master.

Furthermore, if the `reconnectOnError` returns `2`, ioredis will resend the failed command after reconnecting.

## Connection Events
The Redis instance will emit some events about the state of the connection to the Redis server.

Expand Down
4 changes: 3 additions & 1 deletion lib/redis.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ try {
* });
* ```
* @param {function} [options.retryStrategy] - See "Quick Start" section
* @param {function} [options.reconnectOnError] - See "Quick Start" section
* @extends [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)
* @extends Commander
* @example
Expand Down Expand Up @@ -169,7 +170,8 @@ Redis.defaultOptions = {
autoResubscribe: true,
autoResendUnfulfilledCommands: true,
lazyConnect: false,
keyPrefix: ''
keyPrefix: '',
reconnectOnError: null
};

Redis.prototype.resetCommandQueue = function () {
Expand Down
32 changes: 28 additions & 4 deletions lib/redis/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,38 @@ exports.initParser = function () {
};

exports.returnError = function (err) {
var command = this.commandQueue.shift().command;
var item = this.commandQueue.shift();

err.command = {
name: command.name,
args: command.args
name: item.command.name,
args: item.command.args
};

command.reject(err);
var needReconnect = false;
if (this.options.reconnectOnError) {
needReconnect = this.options.reconnectOnError(err);
}

switch (needReconnect) {
case 1:
case true:
if (this.status !== 'reconnecting') {
this.disconnect(true);
}
item.command.reject(err);
break;
case 2:
if (this.status !== 'reconnecting') {
this.disconnect(true);
}
if (this.condition.select !== item.select && item.command.name !== 'select') {
this.select(item.select);
}
this.sendCommand(item.command);
break;
default:
item.command.reject(err);
}
};

var sharedBuffers = {};
Expand Down
106 changes: 106 additions & 0 deletions test/functional/reconnect_on_error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
'use strict';

describe('reconnectOnError', function () {
it('should pass the error as the first param', function (done) {
var pending = 2;
function assert(err) {
expect(err.name).to.eql('ReplyError');
expect(err.command.name).to.eql('set');
expect(err.command.args).to.eql(['foo']);
if (!--pending) {
done();
}
}
var redis = new Redis({
reconnectOnError: function (err) {
assert(err);
}
});

redis.set('foo', function (err) {
assert(err);
});
});

it('should not reconnect if reconnectOnError returns false', function (done) {
var redis = new Redis({
reconnectOnError: function (err) {
return false;
}
});

redis.disconnect = function () {
throw new Error('should not disconnect');
};

redis.set('foo', function (err) {
done();
});
});

it('should reconnect if reconnectOnError returns true or 1', function (done) {
var redis = new Redis({
reconnectOnError: function () {
return true;
}
});

redis.set('foo', function () {
redis.on('ready', function () {
done();
});
});
});

it('should reconnect and retry the command if reconnectOnError returns 2', function (done) {
var redis = new Redis({
reconnectOnError: function () {
redis.del('foo');
return 2;
}
});

redis.set('foo', 'bar');
redis.sadd('foo', 'a', function (err, res) {
expect(res).to.eql(1);
done();
});
});

it('should select the currect database', function (done) {
var redis = new Redis({
reconnectOnError: function () {
redis.select(3);
redis.del('foo');
redis.select(0);
return 2;
}
});

redis.select(3);
redis.set('foo', 'bar');
redis.sadd('foo', 'a', function (err, res) {
expect(res).to.eql(1);
redis.select(3);
redis.type('foo', function (err, type) {
expect(type).to.eql('set');
done();
});
});
});

it('should work with pipeline', function (done) {
var redis = new Redis({
reconnectOnError: function () {
redis.del('foo');
return 2;
}
});

redis.set('foo', 'bar');
redis.pipeline().get('foo').sadd('foo', 'a').exec(function (err, res) {
expect(res).to.eql([[null, 'bar'], [null, 1]]);
done();
});
});
});

0 comments on commit bdd9636

Please sign in to comment.