Skip to content

Commit b1a4b62

Browse files
committed
feat(cluster): redirect on TRYAGAIN error
1 parent c4fee4f commit b1a4b62

7 files changed

Lines changed: 109 additions & 96 deletions

File tree

API.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ Creates a Redis Cluster instance
221221
| [options.maxRedirections] | <code>number</code> | <code>16</code> | When a MOVED or ASK error is received, client will redirect the command to another node. This option limits the max redirections allowed to send a command. |
222222
| [options.retryDelayOnFailover] | <code>number</code> | <code>100</code> | When an error is received when sending a command(e.g. "Connection is closed." when the target Redis node is down), |
223223
| [options.retryDelayOnClusterDown] | <code>number</code> | <code>100</code> | When a CLUSTERDOWN error is received, client will retry if `retryDelayOnClusterDown` is valid delay time. |
224+
| [options.retryDelayOnTryAgain] | <code>number</code> | <code>100</code> | When a TRYAGAIN error is received, client will retry if `retryDelayOnTryAgain` is valid delay time. |
224225
| [options.redisOptions] | <code>Object</code> | | Passed to the constructor of `Redis`. |
225226

226227
<a name="Cluster+connect"></a>

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,8 @@ but a few so that if one is unreachable the client will try the next one, and th
678678
to insure that no command will fail during a failover.
679679
* `retryDelayOnClusterDown`: When a cluster is down, all commands will be rejected with the error of `CLUSTERDOWN`. If this option is a number (by default, it is `100`), the client
680680
will resend the commands after the specified time (in ms).
681+
* `retryDelayOnTryAgain`: If this option is a number (by default, it is `100`), the client
682+
will resend the commands rejected with `TRYAGAIN` error after the specified time (in ms).
681683
* `redisOptions`: Default options passed to the constructor of `Redis` when connecting to a node.
682684

683685
### Read-write splitting

lib/cluster/delay_queue.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict';
2+
3+
var Deque = require('double-ended-queue');
4+
var debug = require('debug')('ioredis:delayqueue');
5+
6+
function DelayQueue() {
7+
this.queues = {};
8+
this.timeouts = {};
9+
}
10+
11+
DelayQueue.prototype.push = function (bucket, item, options) {
12+
var callback = options.callback || process.nextTick;
13+
if (!this.queues[bucket]) {
14+
this.queues[bucket] = new Deque();
15+
}
16+
17+
var queue = this.queues[bucket];
18+
queue.push(item);
19+
20+
if (!this.timeouts[bucket]) {
21+
var _this = this;
22+
this.timeouts[bucket] = setTimeout(function () {
23+
callback(function () {
24+
_this.timeouts[bucket] = null;
25+
_this._execute(bucket);
26+
});
27+
}, options.timeout);
28+
}
29+
};
30+
31+
DelayQueue.prototype._execute = function (bucket) {
32+
var queue = this.queues[bucket];
33+
if (!queue) {
34+
return;
35+
}
36+
var length = queue.length;
37+
if (!length) {
38+
return;
39+
}
40+
debug('send %d commands in %s queue', length, bucket);
41+
42+
this.queues[bucket] = null;
43+
while (queue.length > 0) {
44+
queue.shift()();
45+
}
46+
};
47+
48+
module.exports = DelayQueue;

lib/cluster/index.js

Lines changed: 19 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ var Commander = require('../commander');
1313
var Command = require('../command');
1414
var commands = require('redis-commands');
1515
var ConnectionPool = require('./connection_pool');
16+
var DelayQueue = require('./delay_queue');
1617

1718
/**
1819
* Creates a Redis Cluster instance
@@ -32,6 +33,8 @@ var ConnectionPool = require('./connection_pool');
3233
* "Connection is closed." when the target Redis node is down),
3334
* @param {number} [options.retryDelayOnClusterDown=100] - When a CLUSTERDOWN error is received, client will retry
3435
* if `retryDelayOnClusterDown` is valid delay time.
36+
* @param {number} [options.retryDelayOnTryAgain=100] - When a TRYAGAIN error is received, client will retry
37+
* if `retryDelayOnTryAgain` is valid delay time.
3538
* @param {Object} [options.redisOptions] - Passed to the constructor of `Redis`.
3639
* @extends [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)
3740
* @extends Commander
@@ -70,8 +73,7 @@ function Cluster(startupNodes, options) {
7073
this.retryAttempts = 0;
7174

7275
this.resetOfflineQueue();
73-
this.resetFailoverQueue();
74-
this.resetClusterDownQueue();
76+
this.delayQueue = new DelayQueue();
7577

7678
this.subscriber = null;
7779

@@ -93,7 +95,8 @@ Cluster.defaultOptions = {
9395
scaleReads: 'master',
9496
maxRedirections: 16,
9597
retryDelayOnFailover: 100,
96-
retryDelayOnClusterDown: 100
98+
retryDelayOnClusterDown: 100,
99+
retryDelayOnTryAgain: 100
97100
};
98101

99102
util.inherits(Cluster, EventEmitter);
@@ -103,14 +106,6 @@ Cluster.prototype.resetOfflineQueue = function () {
103106
this.offlineQueue = new Deque();
104107
};
105108

106-
Cluster.prototype.resetFailoverQueue = function () {
107-
this.failoverQueue = new Deque();
108-
};
109-
110-
Cluster.prototype.resetClusterDownQueue = function () {
111-
this.clusterDownQueue = new Deque();
112-
};
113-
114109
/**
115110
* Connect to a cluster
116111
*
@@ -365,30 +360,6 @@ Cluster.prototype.executeOfflineCommands = function () {
365360
}
366361
};
367362

368-
Cluster.prototype.executeFailoverCommands = function () {
369-
if (this.failoverQueue.length) {
370-
debug('send %d commands in failover queue', this.failoverQueue.length);
371-
var failoverQueue = this.failoverQueue;
372-
this.resetFailoverQueue();
373-
while (failoverQueue.length > 0) {
374-
var item = failoverQueue.shift();
375-
item();
376-
}
377-
}
378-
};
379-
380-
Cluster.prototype.executeClusterDownCommands = function () {
381-
if (this.clusterDownQueue.length) {
382-
debug('send %d commands in cluster down queue', this.clusterDownQueue.length);
383-
var clusterDownQueue = this.clusterDownQueue;
384-
this.resetClusterDownQueue();
385-
while (clusterDownQueue.length > 0) {
386-
var item = clusterDownQueue.shift();
387-
item();
388-
}
389-
}
390-
};
391-
392363
Cluster.prototype.sendCommand = function (command, stream, node) {
393364
if (this.status === 'end') {
394365
command.reject(new Error('Connection is closed.'));
@@ -427,6 +398,7 @@ Cluster.prototype.sendCommand = function (command, stream, node) {
427398
debug('command %s is required to ask %s:%s', command.name, key);
428399
tryConnection(false, key);
429400
},
401+
tryagain: partialTry,
430402
clusterDown: partialTry,
431403
connectionClosed: partialTry,
432404
maxRedirections: function (redirectionError) {
@@ -511,7 +483,6 @@ Cluster.prototype.sendCommand = function (command, stream, node) {
511483
};
512484

513485
Cluster.prototype.handleError = function (error, ttl, handlers) {
514-
var _this = this;
515486
if (typeof ttl.value === 'undefined') {
516487
ttl.value = this.options.maxRedirections;
517488
} else {
@@ -524,26 +495,20 @@ Cluster.prototype.handleError = function (error, ttl, handlers) {
524495
var errv = error.message.split(' ');
525496
if (errv[0] === 'MOVED' || errv[0] === 'ASK') {
526497
handlers[errv[0] === 'MOVED' ? 'moved' : 'ask'](errv[1], errv[2]);
498+
} else if (errv[0] === 'TRYAGAIN') {
499+
this.delayQueue.push('tryagain', handlers.tryagain, {
500+
timeout: this.options.retryDelayOnTryAgain
501+
});
527502
} else if (errv[0] === 'CLUSTERDOWN' && this.options.retryDelayOnClusterDown > 0) {
528-
this.clusterDownQueue.push(handlers.clusterDown);
529-
if (!this.clusterDownTimeout) {
530-
this.clusterDownTimeout = setTimeout(function () {
531-
_this.refreshSlotsCache(function () {
532-
_this.clusterDownTimeout = null;
533-
_this.executeClusterDownCommands();
534-
});
535-
}, this.options.retryDelayOnClusterDown);
536-
}
503+
this.delayQueue.push('clusterdown', handlers.connectionClosed, {
504+
timeout: this.options.retryDelayOnClusterDown,
505+
callback: this.refreshSlotsCache.bind(this)
506+
});
537507
} else if (error.message === 'Connection is closed.' && this.options.retryDelayOnFailover > 0) {
538-
this.failoverQueue.push(handlers.connectionClosed);
539-
if (!this.failoverTimeout) {
540-
this.failoverTimeout = setTimeout(function () {
541-
_this.refreshSlotsCache(function () {
542-
_this.failoverTimeout = null;
543-
_this.executeFailoverCommands();
544-
});
545-
}, this.options.retryDelayOnFailover);
546-
}
508+
this.delayQueue.push('failover', handlers.connectionClosed, {
509+
timeout: this.options.retryDelayOnFailover,
510+
callback: this.refreshSlotsCache.bind(this)
511+
});
547512
} else {
548513
handlers.defaults();
549514
}

lib/pipeline.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ Pipeline.prototype.fillResult = function (value, position) {
102102
if (typeof this.leftRedirections === 'undefined') {
103103
this.leftRedirections = {};
104104
}
105+
var exec = function () {
106+
_this.exec();
107+
};
105108
this.redis.handleError(commonError, this.leftRedirections, {
106109
moved: function (slot, key) {
107110
_this.preferKey = key;
@@ -113,12 +116,9 @@ Pipeline.prototype.fillResult = function (value, position) {
113116
_this.preferKey = key;
114117
_this.exec();
115118
},
116-
clusterDown: function () {
117-
_this.exec();
118-
},
119-
connectionClosed: function () {
120-
_this.exec();
121-
},
119+
tryagain: exec,
120+
clusterDown: exec,
121+
connectionClosed: exec,
122122
maxRedirections: function () {
123123
matched = false;
124124
},

test/functional/cluster.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ describe('cluster', function () {
331331

332332
var cluster = new Redis.Cluster([
333333
{ host: '127.0.0.1', port: '30001' }
334-
], { lazyConnect: false });
334+
]);
335335
cluster.get('foo', function () {
336336
cluster.get('foo');
337337
});
@@ -361,7 +361,7 @@ describe('cluster', function () {
361361
});
362362
var cluster = new Redis.Cluster([
363363
{ host: '127.0.0.1', port: '30001' }
364-
], { lazyConnect: false });
364+
], { retryDelayOnFailover: 1 });
365365
cluster.get('foo', function (err, res) {
366366
expect(res).to.eql('bar');
367367
cluster.disconnect();
@@ -453,6 +453,37 @@ describe('cluster', function () {
453453
});
454454
});
455455

456+
describe('TRYAGAIN', function () {
457+
it('should retry the command', function (done) {
458+
var times = 0;
459+
var slotTable = [
460+
[0, 16383, ['127.0.0.1', 30001]]
461+
];
462+
var server = new MockServer(30001, function (argv) {
463+
if (argv[0] === 'cluster' && argv[1] === 'slots') {
464+
return slotTable;
465+
}
466+
if (argv[0] === 'get' && argv[1] === 'foo') {
467+
if (times++ === 1) {
468+
process.nextTick(function () {
469+
cluster.disconnect();
470+
disconnect([server], done);
471+
});
472+
} else {
473+
return new Error('TRYAGAIN Multiple keys request during rehashing of slot');
474+
}
475+
}
476+
});
477+
478+
var cluster = new Redis.Cluster([
479+
{ host: '127.0.0.1', port: '30001' }
480+
], { retryDelayOnTryAgain: 1 });
481+
cluster.get('foo', function () {
482+
cluster.get('foo');
483+
});
484+
});
485+
});
486+
456487
describe('CLUSTERDOWN', function () {
457488
it('should redirect the command to a random node', function (done) {
458489
var slotTable = [

test/unit/cluster.js

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,38 +21,4 @@ describe('cluster', function () {
2121
expect(cluster.options).to.have.property('showFriendlyErrorStack', false);
2222
expect(cluster.options).to.have.property('scaleReads', 'master');
2323
});
24-
25-
describe('#executeFailoverCommands', function () {
26-
it('should execute the commands', function (done) {
27-
var cluster = {
28-
resetFailoverQueue: function () {
29-
this.failoverQueue = [];
30-
},
31-
failoverQueue: []
32-
};
33-
34-
cluster.failoverQueue.push(function () {
35-
expect(this.failoverQueue).to.have.length(0);
36-
done();
37-
}.bind(cluster));
38-
Cluster.prototype.executeFailoverCommands.call(cluster);
39-
});
40-
});
41-
42-
describe('#executeClusterDownCommands', function () {
43-
it('should execute the commands', function (done) {
44-
var cluster = {
45-
resetClusterDownQueue: function () {
46-
this.clusterDownQueue = [];
47-
},
48-
clusterDownQueue: []
49-
};
50-
51-
cluster.clusterDownQueue.push(function () {
52-
expect(this.clusterDownQueue).to.have.length(0);
53-
done();
54-
}.bind(cluster));
55-
Cluster.prototype.executeClusterDownCommands.call(cluster);
56-
});
57-
});
5824
});

0 commit comments

Comments
 (0)