Skip to content

Commit 1babc13

Browse files
committed
feat: add maxRetriesPerRequest option to limit the retries attempts per command
#634, #61 BREAKING CHANGE: The maxRetriesPerRequest is set to 20 instead of null (same behavior as ioredis v3) by default. So when a redis server is down, pending commands won't wait forever until the connection become alive, instead, they only wait about 10s (depends on the retryStrategy option)
1 parent 4e91a48 commit 1babc13

File tree

6 files changed

+103
-1
lines changed

6 files changed

+103
-1
lines changed

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,16 @@ This behavior can be disabled by setting the `autoResubscribe` option to `false`
546546
And if the previous connection has some unfulfilled commands (most likely blocking commands such as `brpop` and `blpop`),
547547
the client will resend them when reconnected. This behavior can be disabled by setting the `autoResendUnfulfilledCommands` option to `false`.
548548

549+
By default, all pending commands will be flushed with an error every 20 retry attempts. That makes sure commands won't wait forever when the connection is down. You can change this behavior by setting `maxRetriesPerRequest`:
550+
551+
```javascript
552+
var redis = new Redis({
553+
maxRetriesPerRequest: 1
554+
});
555+
```
556+
557+
Set maxRetriesPerRequest to `null` to disable this behavior, and every command will wait forever until the connection is alive again (which is the default behavior before ioredis v4).
558+
549559
### Reconnect on error
550560

551561
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:
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = class MaxRetriesPerRequestError extends Error {
2+
constructor (maxRetriesPerRequest) {
3+
var message = `Reached the max retries per request limit (which is ${maxRetriesPerRequest}). Refer to "maxRetriesPerRequest" option for details.`;
4+
5+
super(message);
6+
this.name = this.constructor.name;
7+
Error.captureStackTrace(this, this.constructor);
8+
}
9+
};
10+

lib/errors/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
exports.MaxRetriesPerRequestError = require('./MaxRetriesPerRequestError')

lib/redis.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,8 @@ Redis.defaultOptions = {
176176
keyPrefix: '',
177177
reconnectOnError: null,
178178
readOnly: false,
179-
stringNumbers: false
179+
stringNumbers: false,
180+
maxRetriesPerRequest: 20
180181
};
181182

182183
Redis.prototype.resetCommandQueue = function () {

lib/redis/event_handler.js

+16
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var debug = require('../utils/debug')('ioredis:connection');
44
var Command = require('../command');
55
var utils = require('../utils');
66
var _ = require('../utils/lodash');
7+
var {MaxRetriesPerRequestError} = require('../errors')
78

89
exports.connectHandler = function (self) {
910
return function () {
@@ -94,6 +95,21 @@ exports.closeHandler = function (self) {
9495
self.reconnectTimeout = null;
9596
self.connect().catch(_.noop);
9697
}, retryDelay);
98+
99+
var {maxRetriesPerRequest} = self.options;
100+
if (typeof maxRetriesPerRequest === 'number') {
101+
if (maxRetriesPerRequest < 0) {
102+
debug('maxRetriesPerRequest is negative, ignoring...')
103+
} else {
104+
var remainder = self.retryAttempts % (maxRetriesPerRequest + 1);
105+
if (remainder === 0) {
106+
debug('reach maxRetriesPerRequest limitation, flushing command queue...');
107+
self.flushQueue(
108+
new MaxRetriesPerRequestError(maxRetriesPerRequest)
109+
);
110+
}
111+
}
112+
}
97113
};
98114

99115
function close() {
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use strict';
2+
3+
var {MaxRetriesPerRequestError} = require('../../lib/errors')
4+
5+
describe('maxRetriesPerRequest', function () {
6+
it('throw the correct error when reached the limit', function (done) {
7+
var redis = new Redis(9999, {
8+
retryStrategy() {
9+
return 1
10+
}
11+
});
12+
redis.get('foo', (err) => {
13+
expect(err).instanceOf(MaxRetriesPerRequestError)
14+
done()
15+
})
16+
})
17+
18+
it('defaults to max 20 retries', function (done) {
19+
var redis = new Redis(9999, {
20+
retryStrategy() {
21+
return 1
22+
}
23+
});
24+
redis.get('foo', () => {
25+
expect(redis.retryAttempts).to.eql(21)
26+
redis.get('foo', () => {
27+
expect(redis.retryAttempts).to.eql(42)
28+
done()
29+
})
30+
})
31+
});
32+
33+
it('can be changed', function (done) {
34+
var redis = new Redis(9999, {
35+
maxRetriesPerRequest: 1,
36+
retryStrategy() {
37+
return 1
38+
}
39+
});
40+
redis.get('foo', () => {
41+
expect(redis.retryAttempts).to.eql(2)
42+
redis.get('foo', () => {
43+
expect(redis.retryAttempts).to.eql(4)
44+
done()
45+
})
46+
})
47+
});
48+
49+
it('allows 0', function (done) {
50+
var redis = new Redis(9999, {
51+
maxRetriesPerRequest: 0,
52+
retryStrategy() {
53+
return 1
54+
}
55+
});
56+
redis.get('foo', () => {
57+
expect(redis.retryAttempts).to.eql(1)
58+
redis.get('foo', () => {
59+
expect(redis.retryAttempts).to.eql(2)
60+
done()
61+
})
62+
})
63+
});
64+
});

0 commit comments

Comments
 (0)