Skip to content

Commit cc540db

Browse files
author
Ruben Bridgewater
committed
Implement retry_strategy and add more info to the reconnect event
1 parent 5e8a87b commit cc540db

File tree

2 files changed

+100
-13
lines changed

2 files changed

+100
-13
lines changed

index.js

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ function RedisClient (options) {
103103
this.old_state = null;
104104
this.send_anyway = false;
105105
this.pipeline = 0;
106+
this.times_connected = 0;
106107
this.options = options;
107108
// Init parser
108109
this.reply_parser = new Parser({
@@ -145,14 +146,15 @@ RedisClient.prototype.create_stream = function () {
145146
if (this.options.connect_timeout) {
146147
this.stream.setTimeout(this.connect_timeout, function () {
147148
self.retry_totaltime = self.connect_timeout;
148-
self.connection_gone('timeout');
149+
self.connection_gone('timeout', new Error('Redis connection gone from timeout event'));
149150
});
150151
}
151152

152153
/* istanbul ignore next: travis does not work with stunnel atm. Therefor the tls tests are skipped on travis */
153-
var connect_event = this.options.tls ? "secureConnect" : "connect";
154+
var connect_event = this.options.tls ? 'secureConnect' : 'connect';
154155
this.stream.once(connect_event, function () {
155-
this.removeAllListeners("timeout");
156+
this.removeAllListeners('timeout');
157+
self.times_connected++;
156158
self.on_connect();
157159
});
158160

@@ -166,17 +168,18 @@ RedisClient.prototype.create_stream = function () {
166168
self.on_error(err);
167169
});
168170

169-
/* istanbul ignore next: travis does not work with stunnel atm. Therefor the tls tests are skipped on travis */
171+
/* istanbul ignore next: difficult to test and not important as long as we keep this listener */
170172
this.stream.on('clientError', function (err) {
173+
debug('clientError occured');
171174
self.on_error(err);
172175
});
173176

174177
this.stream.once('close', function () {
175-
self.connection_gone('close');
178+
self.connection_gone('close', new Error('Stream connection closed'));
176179
});
177180

178181
this.stream.once('end', function () {
179-
self.connection_gone('end');
182+
self.connection_gone('end', new Error('Stream connection ended'));
180183
});
181184

182185
this.stream.on('drain', function () {
@@ -268,10 +271,14 @@ RedisClient.prototype.on_error = function (err) {
268271

269272
this.connected = false;
270273
this.ready = false;
271-
this.emit('error', err);
274+
275+
// Only emit the error if the retry_stategy option is not set
276+
if (!this.options.retry_strategy) {
277+
this.emit('error', err);
278+
}
272279
// 'error' events get turned into exceptions if they aren't listened for. If the user handled this error
273280
// then we should try to reconnect.
274-
this.connection_gone('error');
281+
this.connection_gone('error', err);
275282
};
276283

277284
RedisClient.prototype.on_connect = function () {
@@ -417,12 +424,15 @@ RedisClient.prototype.send_offline_queue = function () {
417424
this.offline_queue = new Queue();
418425
};
419426

420-
var retry_connection = function (self) {
427+
var retry_connection = function (self, error) {
421428
debug('Retrying connection...');
422429

423430
self.emit('reconnecting', {
424431
delay: self.retry_delay,
425-
attempt: self.attempts
432+
attempt: self.attempts,
433+
error: error,
434+
times_connected: self.times_connected,
435+
total_retry_time: self.retry_totaltime
426436
});
427437

428438
self.retry_totaltime += self.retry_delay;
@@ -432,8 +442,7 @@ var retry_connection = function (self) {
432442
self.retry_timer = null;
433443
};
434444

435-
RedisClient.prototype.connection_gone = function (why) {
436-
var error;
445+
RedisClient.prototype.connection_gone = function (why, error) {
437446
// If a retry is already in progress, just let that happen
438447
if (this.retry_timer) {
439448
return;
@@ -469,6 +478,25 @@ RedisClient.prototype.connection_gone = function (why) {
469478
return;
470479
}
471480

481+
if (typeof this.options.retry_strategy === 'function') {
482+
this.retry_delay = this.options.retry_strategy({
483+
attempt: this.attempts,
484+
error: error,
485+
total_retry_time: this.retry_totaltime,
486+
times_connected: this.times_connected
487+
});
488+
if (typeof this.retry_delay !== 'number') {
489+
// Pass individual error through
490+
if (this.retry_delay instanceof Error) {
491+
error = this.retry_delay;
492+
}
493+
this.flush_and_error(error);
494+
this.emit('error', error);
495+
this.end(false);
496+
return;
497+
}
498+
}
499+
472500
if (this.max_attempts !== 0 && this.attempts >= this.max_attempts || this.retry_totaltime >= this.connect_timeout) {
473501
var message = this.retry_totaltime >= this.connect_timeout ?
474502
'connection timeout exceeded.' :
@@ -502,7 +530,7 @@ RedisClient.prototype.connection_gone = function (why) {
502530

503531
debug('Retry connection in ' + this.retry_delay + ' ms');
504532

505-
this.retry_timer = setTimeout(retry_connection, this.retry_delay, this);
533+
this.retry_timer = setTimeout(retry_connection, this.retry_delay, this, error);
506534
};
507535

508536
RedisClient.prototype.return_error = function (err) {

test/connection.spec.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,65 @@ describe("connection tests", function () {
127127
client.stream.destroy();
128128
});
129129
});
130+
131+
it("retry_strategy used to reconnect with individual error", function (done) {
132+
var text = '';
133+
var unhookIntercept = intercept(function (data) {
134+
text += data;
135+
return '';
136+
});
137+
var end = helper.callFuncAfter(done, 2);
138+
client = redis.createClient({
139+
retry_strategy: function (options) {
140+
if (options.total_retry_time > 150) {
141+
client.set('foo', 'bar', function (err, res) {
142+
assert.strictEqual(err.message, 'Connection timeout');
143+
end();
144+
});
145+
// Pass a individual error message to the error handler
146+
return new Error('Connection timeout');
147+
}
148+
return Math.min(options.attempt * 25, 200);
149+
},
150+
max_attempts: 5,
151+
retry_max_delay: 123,
152+
port: 9999
153+
});
154+
155+
client.on('error', function(err) {
156+
unhookIntercept();
157+
assert.strictEqual(
158+
text,
159+
'node_redis: WARNING: You activated the retry_strategy and max_attempts at the same time. This is not possible and max_attempts will be ignored.\n' +
160+
'node_redis: WARNING: You activated the retry_strategy and retry_max_delay at the same time. This is not possible and retry_max_delay will be ignored.\n'
161+
);
162+
assert.strictEqual(err.message, 'Connection timeout');
163+
assert(!err.code);
164+
end();
165+
});
166+
});
167+
168+
it("retry_strategy used to reconnect", function (done) {
169+
var end = helper.callFuncAfter(done, 2);
170+
client = redis.createClient({
171+
retry_strategy: function (options) {
172+
if (options.total_retry_time > 150) {
173+
client.set('foo', 'bar', function (err, res) {
174+
assert.strictEqual(err.code, 'ECONNREFUSED');
175+
end();
176+
});
177+
return false;
178+
}
179+
return Math.min(options.attempt * 25, 200);
180+
},
181+
port: 9999
182+
});
183+
184+
client.on('error', function(err) {
185+
assert.strictEqual(err.code, 'ECONNREFUSED');
186+
end();
187+
});
188+
});
130189
});
131190

132191
describe("when not connected", function () {

0 commit comments

Comments
 (0)