From 1babc13b6b9634eaf4bbe6fea1ab28ddfeffba33 Mon Sep 17 00:00:00 2001 From: luin Date: Mon, 25 Jun 2018 01:53:20 +0800 Subject: [PATCH] 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) --- README.md | 10 ++++ lib/errors/MaxRetriesPerRequestError.js | 10 ++++ lib/errors/index.js | 1 + lib/redis.js | 3 +- lib/redis/event_handler.js | 16 +++++++ test/functional/maxRetriesPerRequest.js | 64 +++++++++++++++++++++++++ 6 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 lib/errors/MaxRetriesPerRequestError.js create mode 100644 lib/errors/index.js create mode 100644 test/functional/maxRetriesPerRequest.js diff --git a/README.md b/README.md index e49791ec..09aa47fd 100644 --- a/README.md +++ b/README.md @@ -546,6 +546,16 @@ 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`. +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`: + +```javascript +var redis = new Redis({ + maxRetriesPerRequest: 1 +}); +``` + +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). + ### 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: diff --git a/lib/errors/MaxRetriesPerRequestError.js b/lib/errors/MaxRetriesPerRequestError.js new file mode 100644 index 00000000..6e92e3be --- /dev/null +++ b/lib/errors/MaxRetriesPerRequestError.js @@ -0,0 +1,10 @@ +module.exports = class MaxRetriesPerRequestError extends Error { + constructor (maxRetriesPerRequest) { + var message = `Reached the max retries per request limit (which is ${maxRetriesPerRequest}). Refer to "maxRetriesPerRequest" option for details.`; + + super(message); + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +}; + diff --git a/lib/errors/index.js b/lib/errors/index.js new file mode 100644 index 00000000..6a342fd4 --- /dev/null +++ b/lib/errors/index.js @@ -0,0 +1 @@ +exports.MaxRetriesPerRequestError = require('./MaxRetriesPerRequestError') diff --git a/lib/redis.js b/lib/redis.js index c4263698..fc5addd5 100644 --- a/lib/redis.js +++ b/lib/redis.js @@ -176,7 +176,8 @@ Redis.defaultOptions = { keyPrefix: '', reconnectOnError: null, readOnly: false, - stringNumbers: false + stringNumbers: false, + maxRetriesPerRequest: 20 }; Redis.prototype.resetCommandQueue = function () { diff --git a/lib/redis/event_handler.js b/lib/redis/event_handler.js index 4345eff7..77805594 100644 --- a/lib/redis/event_handler.js +++ b/lib/redis/event_handler.js @@ -4,6 +4,7 @@ var debug = require('../utils/debug')('ioredis:connection'); var Command = require('../command'); var utils = require('../utils'); var _ = require('../utils/lodash'); +var {MaxRetriesPerRequestError} = require('../errors') exports.connectHandler = function (self) { return function () { @@ -94,6 +95,21 @@ exports.closeHandler = function (self) { self.reconnectTimeout = null; self.connect().catch(_.noop); }, retryDelay); + + var {maxRetriesPerRequest} = self.options; + if (typeof maxRetriesPerRequest === 'number') { + if (maxRetriesPerRequest < 0) { + debug('maxRetriesPerRequest is negative, ignoring...') + } else { + var remainder = self.retryAttempts % (maxRetriesPerRequest + 1); + if (remainder === 0) { + debug('reach maxRetriesPerRequest limitation, flushing command queue...'); + self.flushQueue( + new MaxRetriesPerRequestError(maxRetriesPerRequest) + ); + } + } + } }; function close() { diff --git a/test/functional/maxRetriesPerRequest.js b/test/functional/maxRetriesPerRequest.js new file mode 100644 index 00000000..0e99e769 --- /dev/null +++ b/test/functional/maxRetriesPerRequest.js @@ -0,0 +1,64 @@ +'use strict'; + +var {MaxRetriesPerRequestError} = require('../../lib/errors') + +describe('maxRetriesPerRequest', function () { + it('throw the correct error when reached the limit', function (done) { + var redis = new Redis(9999, { + retryStrategy() { + return 1 + } + }); + redis.get('foo', (err) => { + expect(err).instanceOf(MaxRetriesPerRequestError) + done() + }) + }) + + it('defaults to max 20 retries', function (done) { + var redis = new Redis(9999, { + retryStrategy() { + return 1 + } + }); + redis.get('foo', () => { + expect(redis.retryAttempts).to.eql(21) + redis.get('foo', () => { + expect(redis.retryAttempts).to.eql(42) + done() + }) + }) + }); + + it('can be changed', function (done) { + var redis = new Redis(9999, { + maxRetriesPerRequest: 1, + retryStrategy() { + return 1 + } + }); + redis.get('foo', () => { + expect(redis.retryAttempts).to.eql(2) + redis.get('foo', () => { + expect(redis.retryAttempts).to.eql(4) + done() + }) + }) + }); + + it('allows 0', function (done) { + var redis = new Redis(9999, { + maxRetriesPerRequest: 0, + retryStrategy() { + return 1 + } + }); + redis.get('foo', () => { + expect(redis.retryAttempts).to.eql(1) + redis.get('foo', () => { + expect(redis.retryAttempts).to.eql(2) + done() + }) + }) + }); +});