Commit 889fe9e0 authored by Nick Stenning's avatar Nick Stenning

Ensure websocket always waits before reconnecting

Currently, if the websocket connection fails, then we will always
immediately attempt a reconnect. This can easily contribute to a
thundering herd problem if all WebSocket clients (or even a large
portion of them) are disconnected simultaneously.

This commit ensures that an aborted connection always results in at
least a 1s-2s (randomized) delay.
parent ae7b85a7
'use strict';
var Socket = require('../websocket'); var Socket = require('../websocket');
describe('websocket wrapper', function () { describe('websocket wrapper', function () {
...@@ -8,7 +10,7 @@ describe('websocket wrapper', function () { ...@@ -8,7 +10,7 @@ describe('websocket wrapper', function () {
this.close = sinon.stub(); this.close = sinon.stub();
this.send = sinon.stub(); this.send = sinon.stub();
fakeSocket = this; fakeSocket = this;
}; }
FakeWebSocket.OPEN = 1; FakeWebSocket.OPEN = 1;
var WebSocket = window.WebSocket; var WebSocket = window.WebSocket;
...@@ -29,11 +31,23 @@ describe('websocket wrapper', function () { ...@@ -29,11 +31,23 @@ describe('websocket wrapper', function () {
}); });
it('should reconnect after an abnormal disconnection', function () { it('should reconnect after an abnormal disconnection', function () {
var socket = new Socket('ws://test:1234'); new Socket('ws://test:1234');
assert.ok(fakeSocket); assert.ok(fakeSocket);
var initialSocket = fakeSocket; var initialSocket = fakeSocket;
fakeSocket.onopen({});
fakeSocket.onclose({code: 1006});
clock.tick(2000);
assert.ok(fakeSocket);
assert.notEqual(fakeSocket, initialSocket);
});
it('should reconnect if initial connection fails', function () {
new Socket('ws://test:1234');
assert.ok(fakeSocket);
var initialSocket = fakeSocket;
fakeSocket.onopen({});
fakeSocket.onclose({code: 1006}); fakeSocket.onclose({code: 1006});
clock.tick(1000); clock.tick(4000);
assert.ok(fakeSocket); assert.ok(fakeSocket);
assert.notEqual(fakeSocket, initialSocket); assert.notEqual(fakeSocket, initialSocket);
}); });
...@@ -57,7 +71,7 @@ describe('websocket wrapper', function () { ...@@ -57,7 +71,7 @@ describe('websocket wrapper', function () {
socket.close(); socket.close();
assert.called(fakeSocket.close); assert.called(fakeSocket.close);
var initialSocket = fakeSocket; var initialSocket = fakeSocket;
clock.tick(1000); clock.tick(2000);
assert.equal(fakeSocket, initialSocket); assert.equal(fakeSocket, initialSocket);
}); });
......
...@@ -7,6 +7,9 @@ var inherits = require('inherits'); ...@@ -7,6 +7,9 @@ var inherits = require('inherits');
// see https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent // see https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
var CLOSE_NORMAL = 1000; var CLOSE_NORMAL = 1000;
// Minimum delay, in ms, before reconnecting after an abnormal connection close.
var RECONNECT_MIN_DELAY = 1000;
/** /**
* Socket is a minimal wrapper around WebSocket which provides: * Socket is a minimal wrapper around WebSocket which provides:
* *
...@@ -25,6 +28,9 @@ function Socket(url) { ...@@ -25,6 +28,9 @@ function Socket(url) {
// the current WebSocket instance // the current WebSocket instance
var socket; var socket;
// a pending operation to connect a WebSocket
var operation;
function sendMessages() { function sendMessages() {
while (messageQueue.length > 0) { while (messageQueue.length > 0) {
var messageString = JSON.stringify(messageQueue.shift()); var messageString = JSON.stringify(messageQueue.shift());
...@@ -32,51 +38,70 @@ function Socket(url) { ...@@ -32,51 +38,70 @@ function Socket(url) {
} }
} }
function reconnect() { // Connect the websocket immediately. If a connection attempt is already in
var didConnect = false; // progress, do nothing.
var connectOperation = retry.operation({ function connect() {
// Wait 2s before attempting to reconnect if (operation) {
minTimeout: 2000, return;
// Don't retry forever }
operation = retry.operation({
minTimeout: RECONNECT_MIN_DELAY * 2,
// Don't retry forever -- fail permanently after 10 retries
retries: 10, retries: 10,
// Randomize retry times to minimise the thundering herd effect // Randomize retry times to minimise the thundering herd effect
randomize: true randomize: true
}); });
connectOperation.attempt(function (currentAttempt) {
operation.attempt(function () {
socket = new WebSocket(url); socket = new WebSocket(url);
socket.onopen = function (event) { socket.onopen = function (event) {
// signal successful connection onOpen();
connectOperation.retry();
didConnect = true;
sendMessages();
self.emit('open', event); self.emit('open', event);
}; };
socket.onclose = function (event) { socket.onclose = function (event) {
if (event.code !== CLOSE_NORMAL) { if (event.code === CLOSE_NORMAL) {
if (didConnect) { self.emit('close', event);
console.warn('The WebSocket connection closed abnormally ' + return;
'(code: %d, reason: %s). Reconnecting automatically.',
event.code, event.reason);
reconnect();
} else {
console.warn('Retrying connection (attempt %d)', currentAttempt);
connectOperation.retry(new Error(event.reason));
}
} }
self.emit('close', event); var err = new Error('WebSocket closed abnormally, code: ' + event.code);
console.warn(err);
onAbnormalClose(err);
}; };
socket.onerror = function (event) { socket.onerror = function (event) {
self.emit('error', event); self.emit('error', event);
}; };
socket.onmessage = function (event) { socket.onmessage = function (event) {
self.emit('message', event); self.emit('message', event);
}; };
}); });
}; }
// onOpen is called when a websocket connection is successfully established.
function onOpen() {
operation = null;
sendMessages();
}
// onAbnormalClose is called when a websocket connection closes abnormally.
// This may be the result of a failure to connect, or an abnormal close after
// a previous successful connection.
function onAbnormalClose(error) {
// If we're already in a reconnection loop, trigger a retry...
if (operation) {
if (!operation.retry(error)) {
console.error('reached max retries attempting to reconnect websocket');
}
return;
}
// ...otherwise reconnect the websocket after a short delay.
var delay = RECONNECT_MIN_DELAY;
delay += Math.floor(Math.random() * delay);
operation = setTimeout(function () {
operation = null;
connect();
}, delay);
}
/** Close the underlying WebSocket connection */ /** Close the underlying WebSocket connection */
this.close = function () { this.close = function () {
...@@ -99,8 +124,7 @@ function Socket(url) { ...@@ -99,8 +124,7 @@ function Socket(url) {
return socket.readyState === WebSocket.OPEN; return socket.readyState === WebSocket.OPEN;
}; };
// establish the initial connection connect();
reconnect();
} }
inherits(Socket, EventEmitter); inherits(Socket, EventEmitter);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment