Skip to content

Commit 57b5c7f

Browse files
committed
Merge pull request #829 from fintura/broken-mode
Implement redis connection broken mode and more shiny things Fixes #569 Fixes #587 Fixes #566 Fixes #586 Fixes #280 This includes the fixes as suggested in #671, #615 and #533. Thx a lot to @qdb, @tobek and @chrishamant Closes #675, #463, #362, #438 and #724
2 parents 403bfb0 + 89c8dd0 commit 57b5c7f

File tree

8 files changed

+243
-82
lines changed

8 files changed

+243
-82
lines changed

.jshintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"mocha": true,
1818

1919
// Relaxing options
20-
"boss": true, // Accept things like `while (command = keys.shift()) { ... }`
20+
"boss": true, // Accept statements like `while (key = keys.pop()) {}`
2121

2222
"overrides": {
2323
"examples/*.js": {

README.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ then replayed just before this event is emitted.
113113
is set. If this options is set, `connect` will be emitted when the stream is connected, and then
114114
you are free to try to send commands.
115115

116+
### "reconnecting"
117+
118+
`client` will emit `reconnecting` when trying to reconnect to the Redis server after losing the connection. Listeners
119+
are passed an object containing `delay` (in ms) and `attempt` (the attempt #) attributes.
120+
116121
### "error"
117122

118123
`client` will emit `error` when encountering an error connecting to the Redis server.
@@ -189,10 +194,11 @@ with an error, or an error will be thrown if no callback is specified.
189194
* `retry_max_delay`: defaults to `null`. By default every time the client tries to connect and fails time before
190195
reconnection (delay) almost doubles. This delay normally grows infinitely, but setting `retry_max_delay` limits delay
191196
to maximum value, provided in milliseconds.
192-
* `connect_timeout` defaults to `false`. By default client will try reconnecting until connected. Setting `connect_timeout`
193-
limits total time for client to reconnect. Value is provided in milliseconds and is counted once the disconnect occured.
194-
* `max_attempts` defaults to `null`. By default client will try reconnecting until connected. Setting `max_attempts`
195-
limits total amount of reconnects.
197+
* `connect_timeout` defaults to `86400000`. Setting `connect_timeout` limits total time for client to reconnect.
198+
Value is provided in milliseconds and is counted once the disconnect occured. The last retry is going to happen exactly at the timeout time.
199+
That way the default is to try reconnecting until 24h passed.
200+
* `max_attempts` defaults to `0`. By default client will try reconnecting until connected. Setting `max_attempts`
201+
limits total amount of connection tries. Setting this to 1 will prevent any reconnect tries.
196202
* `auth_pass` defaults to `null`. By default client will try connecting without auth. If set, client will run redis auth command on connect.
197203
* `family` defaults to `IPv4`. The client connects in IPv4 if not specified or if the DNS resolution returns an IPv4 address.
198204
You can force an IPv6 if you set the family to 'IPv6'. See nodejs net or dns modules how to use the family type.
@@ -576,12 +582,12 @@ some kind of maximum queue depth for pre-connection commands.
576582

577583
## client.retry_delay
578584

579-
Current delay in milliseconds before a connection retry will be attempted. This starts at `250`.
585+
Current delay in milliseconds before a connection retry will be attempted. This starts at `200`.
580586

581587
## client.retry_backoff
582588

583589
Multiplier for future retry timeouts. This should be larger than 1 to add more time between retries.
584-
Defaults to 1.7. The default initial connection retry is 250, so the second retry will be 425, followed by 723.5, etc.
590+
Defaults to 1.7. The default initial connection retry is 200, so the second retry will be 340, followed by 578, etc.
585591

586592
### Commands with Optional and Keyword arguments
587593

index.js

Lines changed: 44 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,11 @@ function RedisClient(stream, options) {
5252
this.should_buffer = false;
5353
this.command_queue_high_water = this.options.command_queue_high_water || 1000;
5454
this.command_queue_low_water = this.options.command_queue_low_water || 0;
55-
if (options.max_attempts && options.max_attempts > 0) {
56-
this.max_attempts = +options.max_attempts;
57-
}
55+
this.max_attempts = +options.max_attempts || 0;
5856
this.command_queue = new Queue(); // holds sent commands to de-pipeline them
5957
this.offline_queue = new Queue(); // holds commands issued but not able to be sent
6058
this.commands_sent = 0;
61-
if (options.connect_timeout && options.connect_timeout > 0) {
62-
this.connect_timeout = +options.connect_timeout;
63-
}
59+
this.connect_timeout = +options.connect_timeout || 86400000; // 24 * 60 * 60 * 1000 ms
6460
this.enable_offline_queue = true;
6561
if (this.options.enable_offline_queue === false) {
6662
this.enable_offline_queue = false;
@@ -123,7 +119,7 @@ RedisClient.prototype.install_stream_listeners = function() {
123119
RedisClient.prototype.initialize_retry_vars = function () {
124120
this.retry_timer = null;
125121
this.retry_totaltime = 0;
126-
this.retry_delay = 150;
122+
this.retry_delay = 200;
127123
this.retry_backoff = 1.7;
128124
this.attempts = 1;
129125
};
@@ -141,21 +137,17 @@ RedisClient.prototype.unref = function () {
141137
};
142138

143139
// flush offline_queue and command_queue, erroring any items with a callback first
144-
RedisClient.prototype.flush_and_error = function (message) {
145-
var command_obj, error;
146-
147-
error = new Error(message);
140+
RedisClient.prototype.flush_and_error = function (error) {
141+
var command_obj;
148142

149-
while (this.offline_queue.length > 0) {
150-
command_obj = this.offline_queue.shift();
143+
while (command_obj = this.offline_queue.shift()) {
151144
if (typeof command_obj.callback === "function") {
152145
command_obj.callback(error);
153146
}
154147
}
155148
this.offline_queue = new Queue();
156149

157-
while (this.command_queue.length > 0) {
158-
command_obj = this.command_queue.shift();
150+
while (command_obj = this.command_queue.shift()) {
159151
if (typeof command_obj.callback === "function") {
160152
command_obj.callback(error);
161153
}
@@ -172,8 +164,6 @@ RedisClient.prototype.on_error = function (msg) {
172164

173165
debug(message);
174166

175-
this.flush_and_error(message);
176-
177167
this.connected = false;
178168
this.ready = false;
179169

@@ -399,8 +389,8 @@ RedisClient.prototype.ready_check = function () {
399389
RedisClient.prototype.send_offline_queue = function () {
400390
var command_obj, buffered_writes = 0;
401391

402-
while (this.offline_queue.length > 0) {
403-
command_obj = this.offline_queue.shift();
392+
// TODO: Implement queue.pop() as it should be faster than shift and evaluate petka antonovs queue
393+
while (command_obj = this.offline_queue.shift()) {
404394
debug("Sending offline command: " + command_obj.command);
405395
buffered_writes += !this.send_command(command_obj.command, command_obj.args, command_obj.callback);
406396
}
@@ -438,56 +428,54 @@ RedisClient.prototype.connection_gone = function (why) {
438428
}
439429

440430
// since we are collapsing end and close, users don't expect to be called twice
441-
if (! this.emitted_end) {
431+
if (!this.emitted_end) {
442432
this.emit("end");
443433
this.emitted_end = true;
444434
}
445435

446-
this.flush_and_error("Redis connection gone from " + why + " event.");
447-
448436
// If this is a requested shutdown, then don't retry
449437
if (this.closing) {
450-
this.retry_timer = null;
451-
debug("Connection ended from quit command, not retrying.");
438+
debug("connection ended from quit command, not retrying.");
439+
this.flush_and_error(new Error("Redis connection gone from " + why + " event."));
440+
return;
441+
}
442+
443+
if (this.max_attempts !== 0 && this.attempts >= this.max_attempts || this.retry_totaltime >= this.connect_timeout) {
444+
var message = this.retry_totaltime >= this.connect_timeout ?
445+
'connection timeout exceeded.' :
446+
'maximum connection attempts exceeded.';
447+
var error = new Error("Redis connection in broken state: " + message);
448+
error.code = 'CONNECTION_BROKEN';
449+
this.flush_and_error(error);
450+
this.emit('error', error);
451+
this.end();
452452
return;
453453
}
454454

455-
var nextDelay = Math.floor(this.retry_delay * this.retry_backoff);
456-
if (this.retry_max_delay !== null && nextDelay > this.retry_max_delay) {
455+
if (this.retry_max_delay !== null && this.retry_delay > this.retry_max_delay) {
457456
this.retry_delay = this.retry_max_delay;
458-
} else {
459-
this.retry_delay = nextDelay;
457+
} else if (this.retry_totaltime + this.retry_delay > this.connect_timeout) {
458+
// Do not exceed the maximum
459+
this.retry_delay = this.connect_timeout - this.retry_totaltime;
460460
}
461461

462462
debug("Retry connection in " + this.retry_delay + " ms");
463463

464-
if (this.max_attempts && this.attempts >= this.max_attempts) {
465-
this.retry_timer = null;
466-
// TODO - some people need a "Redis is Broken mode" for future commands that errors immediately, and others
467-
// want the program to exit. Right now, we just log, which doesn't really help in either case.
468-
debug("Couldn't get Redis connection after " + this.max_attempts + " attempts.");
469-
return;
470-
}
471-
472-
this.attempts += 1;
473-
this.emit("reconnecting", {
474-
delay: self.retry_delay,
475-
attempt: self.attempts
476-
});
477464
this.retry_timer = setTimeout(function () {
478465
debug("Retrying connection...");
479466

480-
self.retry_totaltime += self.retry_delay;
467+
self.emit("reconnecting", {
468+
delay: self.retry_delay,
469+
attempt: self.attempts
470+
});
481471

482-
if (self.connect_timeout && self.retry_totaltime >= self.connect_timeout) {
483-
self.retry_timer = null;
484-
// TODO - engage Redis is Broken mode for future commands, or whatever
485-
debug("Couldn't get Redis connection after " + self.retry_totaltime + "ms.");
486-
return;
487-
}
472+
self.retry_totaltime += self.retry_delay;
473+
self.attempts += 1;
474+
self.retry_delay = Math.round(self.retry_delay * self.retry_backoff);
488475

489476
self.stream = net.createConnection(self.connectionOption);
490477
self.install_stream_listeners();
478+
491479
self.retry_timer = null;
492480
}, this.retry_delay);
493481
};
@@ -836,12 +824,12 @@ RedisClient.prototype.pub_sub_command = function (command_obj) {
836824
RedisClient.prototype.end = function () {
837825
this.stream._events = {};
838826

839-
//clear retry_timer
840-
if(this.retry_timer){
827+
// Clear retry_timer
828+
if (this.retry_timer){
841829
clearTimeout(this.retry_timer);
842-
this.retry_timer=null;
830+
this.retry_timer = null;
843831
}
844-
this.stream.on("error", function(){});
832+
this.stream.on("error", function noop(){});
845833

846834
this.connected = false;
847835
this.ready = false;
@@ -1047,7 +1035,7 @@ Multi.prototype.exec = Multi.prototype.EXEC = function (callback) {
10471035

10481036
// TODO - make this callback part of Multi.prototype instead of creating it each time
10491037
return this._client.send_command("exec", [], function (err, replies) {
1050-
if (err) {
1038+
if (err && !err.code) {
10511039
if (callback) {
10521040
errors.push(err);
10531041
callback(errors);
@@ -1083,6 +1071,9 @@ Multi.prototype.exec = Multi.prototype.EXEC = function (callback) {
10831071

10841072
if (callback) {
10851073
callback(null, replies);
1074+
} else if (err && err.code !== 'CONNECTION_BROKEN') {
1075+
// Exclude CONNECTION_BROKEN so that error won't be emitted twice
1076+
self._client.emit('error', err);
10861077
}
10871078
});
10881079
};

test/auth.spec.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ describe("client authentication", function () {
1212
});
1313
});
1414

15-
helper.allTests(function(parser, ip, args) {
15+
helper.allTests({
16+
allConnections: true
17+
}, function(parser, ip, args) {
1618

1719
describe("using " + parser + " and " + ip, function () {
18-
var args = config.configureClient(parser, ip);
1920
var auth = 'porkchopsandwiches';
2021
var client = null;
2122

test/commands/hgetall.spec.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ describe("The 'hgetall' method", function () {
1313
var client;
1414

1515
describe('regular client', function () {
16-
var args = config.configureClient(parser, ip);
1716

1817
beforeEach(function (done) {
1918
client = redis.createClient.apply(redis.createClient, args);

test/connection.spec.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
'use strict';
2+
3+
var assert = require("assert");
4+
var config = require("./lib/config");
5+
var helper = require('./helper');
6+
var redis = config.redis;
7+
8+
describe("on lost connection", function () {
9+
helper.allTests(function(parser, ip, args) {
10+
11+
describe("using " + parser + " and " + ip, function () {
12+
13+
it("emit an error after max retry attempts and do not try to reconnect afterwards", function (done) {
14+
var max_attempts = 4;
15+
var client = redis.createClient({
16+
parser: parser,
17+
max_attempts: max_attempts
18+
});
19+
var calls = 0;
20+
21+
client.once('ready', function() {
22+
helper.killConnection(client);
23+
});
24+
25+
client.on("reconnecting", function (params) {
26+
calls++;
27+
});
28+
29+
client.on('error', function(err) {
30+
if (/Redis connection in broken state: maximum connection attempts.*?exceeded./.test(err.message)) {
31+
setTimeout(function () {
32+
assert.strictEqual(calls, max_attempts - 1);
33+
done();
34+
}, 1500);
35+
}
36+
});
37+
});
38+
39+
it("emit an error after max retry timeout and do not try to reconnect afterwards", function (done) {
40+
var connect_timeout = 1000; // in ms
41+
var client = redis.createClient({
42+
parser: parser,
43+
connect_timeout: connect_timeout
44+
});
45+
var time = 0;
46+
47+
client.once('ready', function() {
48+
helper.killConnection(client);
49+
});
50+
51+
client.on("reconnecting", function (params) {
52+
time += params.delay;
53+
});
54+
55+
client.on('error', function(err) {
56+
if (/Redis connection in broken state: connection timeout.*?exceeded./.test(err.message)) {
57+
setTimeout(function () {
58+
assert(time === connect_timeout);
59+
done();
60+
}, 1500);
61+
}
62+
});
63+
});
64+
65+
it("end connection while retry is still ongoing", function (done) {
66+
var connect_timeout = 1000; // in ms
67+
var client = redis.createClient({
68+
parser: parser,
69+
connect_timeout: connect_timeout
70+
});
71+
72+
client.once('ready', function() {
73+
helper.killConnection(client);
74+
});
75+
76+
client.on("reconnecting", function (params) {
77+
client.end();
78+
setTimeout(done, 100);
79+
});
80+
});
81+
82+
});
83+
});
84+
});

0 commit comments

Comments
 (0)