Skip to content

Commit 68db71b

Browse files
authored
fix(cluster): make blocking commands works with cluster (#867)
As discussed in #850, we should use a separate connection for refreshing slots to avoid conflict with blocking commands invoked by users. Close #850
1 parent b323c1c commit 68db71b

File tree

5 files changed

+49
-20
lines changed

5 files changed

+49
-20
lines changed

lib/cluster/index.ts

+32-13
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,27 @@ class Cluster extends EventEmitter {
107107
this.offlineQueue = new Deque()
108108
}
109109

110+
clearNodesRefreshInterval() {
111+
if (this.slotsTimer) {
112+
clearTimeout(this.slotsTimer)
113+
this.slotsTimer = null
114+
}
115+
}
116+
110117
resetNodesRefreshInterval() {
111118
if (this.slotsTimer) {
112119
return
113120
}
114-
this.slotsTimer = setInterval(function () {
115-
this.refreshSlotsCache()
116-
}.bind(this), this.options.slotsRefreshInterval)
121+
const nextRound = () => {
122+
this.slotsTimer = setTimeout(() => {
123+
debug('refreshing slot caches... (triggered by "slotsRefreshInterval" option)')
124+
this.refreshSlotsCache(() => {
125+
nextRound()
126+
})
127+
}, this.options.slotsRefreshInterval)
128+
}
129+
130+
nextRound()
117131
}
118132

119133
/**
@@ -245,10 +259,7 @@ class Cluster extends EventEmitter {
245259
this.reconnectTimeout = null
246260
debug('Canceled reconnecting attempts')
247261
}
248-
if (this.slotsTimer) {
249-
clearInterval(this.slotsTimer)
250-
this.slotsTimer = null
251-
}
262+
this.clearNodesRefreshInterval()
252263

253264
this.subscriber.stop()
254265
if (status === 'wait') {
@@ -276,10 +287,7 @@ class Cluster extends EventEmitter {
276287
clearTimeout(this.reconnectTimeout)
277288
this.reconnectTimeout = null
278289
}
279-
if (this.slotsTimer) {
280-
clearInterval(this.slotsTimer)
281-
this.slotsTimer = null
282-
}
290+
this.clearNodesRefreshInterval()
283291

284292
this.subscriber.stop()
285293

@@ -471,6 +479,7 @@ class Cluster extends EventEmitter {
471479
}
472480
_this.connectionPool.findOrCreate(_this.natMapper(key))
473481
tryConnection()
482+
debug('refreshing slot caches... (triggered by MOVED error)')
474483
_this.refreshSlotsCache()
475484
},
476485
ask: function (slot, key) {
@@ -608,9 +617,19 @@ class Cluster extends EventEmitter {
608617
if (!redis) {
609618
return callback(new Error('Node is disconnected'))
610619
}
611-
redis.cluster('slots', timeout((err, result) => {
620+
621+
// Use a duplication of the connection to avoid
622+
// timeouts when the connection is in the blocking
623+
// mode (e.g. waiting for BLPOP).
624+
const duplicatedConnection = redis.duplicate({
625+
enableOfflineQueue: true,
626+
enableReadyCheck: false,
627+
retryStrategy: null,
628+
connectionName: 'ioredisClusterRefresher'
629+
})
630+
duplicatedConnection.cluster('slots', timeout((err, result) => {
631+
duplicatedConnection.disconnect()
612632
if (err) {
613-
redis.disconnect()
614633
return callback(err)
615634
}
616635
if (this.status === 'disconnecting' || this.status === 'close' || this.status === 'end') {

test/functional/cluster/index.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,12 @@ describe('cluster', function () {
104104
return 0;
105105
}
106106
});
107+
let hasDone = false
107108
new MockServer(30002, function () {
109+
if (hasDone) {
110+
return
111+
}
112+
hasDone = true
108113
client.disconnect();
109114
done();
110115
});
@@ -250,8 +255,8 @@ describe('cluster', function () {
250255
describe('#nodes()', function () {
251256
it('should return the corrent nodes', function (done) {
252257
var slotTable = [
253-
[0, 5460, ['127.0.0.1', 30001], ['127.0.0.1', 30003]],
254-
[5461, 10922, ['127.0.0.1', 30002]]
258+
[0, 16381, ['127.0.0.1', 30001], ['127.0.0.1', 30003]],
259+
[16382, 16383, ['127.0.0.1', 30002]]
255260
];
256261
var node = new MockServer(30001, function (argv) {
257262
if (argv[0] === 'cluster' && argv[1] === 'slots') {
@@ -271,7 +276,8 @@ describe('cluster', function () {
271276
});
272277

273278
var cluster = new Redis.Cluster([{ host: '127.0.0.1', port: '30001' }]);
274-
cluster.on('ready', function () {
279+
// Make sure 30001 has been connected
280+
cluster.get('foo', function () {
275281
expect(cluster.nodes()).to.have.lengthOf(3);
276282
expect(cluster.nodes('all')).to.have.lengthOf(3);
277283
expect(cluster.nodes('master')).to.have.lengthOf(2);

test/functional/cluster/moved.js

+4
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ describe('cluster:MOVED', function () {
3030
if (argv[0] === 'get' && argv[1] === 'foo') {
3131
expect(moved).to.eql(false);
3232
moved = true;
33+
slotTable[0][1] = 16381
34+
slotTable[1][0] = 16382
3335
return new Error('MOVED ' + calculateSlot('foo') + ' 127.0.0.1:30001');
3436
}
3537
});
@@ -103,6 +105,8 @@ describe('cluster:MOVED', function () {
103105
if (argv[0] === 'get' && argv[1] === 'foo') {
104106
expect(moved).to.eql(false);
105107
moved = true;
108+
slotTable[0][1] = 16381
109+
slotTable[1][0] = 16382
106110
return new Error('MOVED ' + calculateSlot('foo') + ' 127.0.0.1:30001');
107111
}
108112
});

test/functional/cluster/quit.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ describe('cluster:quit', () => {
3232
it('failed when quit returns error', function (done) {
3333
const ERROR_MESSAGE = 'quit random error'
3434
const slotTable = [
35-
[0, 1000, ['127.0.0.1', 30001]],
36-
[1001, 16383, ['127.0.0.1', 30002]]
35+
[0, 16381, ['127.0.0.1', 30001]],
36+
[16382, 16383, ['127.0.0.1', 30002]]
3737
]
3838
new MockServer(30001, function (argv, c) {
3939
if (argv[0] === 'quit') {
@@ -49,7 +49,7 @@ describe('cluster:quit', () => {
4949
const cluster = new Redis.Cluster([
5050
{ host: '127.0.0.1', port: '30001' }
5151
])
52-
cluster.on('ready', () => {
52+
cluster.get('foo', () => {
5353
cluster.quit((err) => {
5454
expect(err.message).to.eql(ERROR_MESSAGE)
5555
cluster.disconnect()

test/helpers/mock_server.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ MockServer.prototype.write = function (c, data) {
130130

131131
MockServer.prototype.findClientByName = function (name) {
132132
for (const client of this.clients) {
133-
if (client._connectionName === name) {
133+
if (client && client._connectionName === name) {
134134
return client
135135
}
136136
}

0 commit comments

Comments
 (0)