recovery
Version:
Recover from a network failure using randomized exponential backoff
221 lines (184 loc) • 6.25 kB
JavaScript
var EventEmitter = require('eventemitter3')
, millisecond = require('millisecond')
, destroy = require('demolish')
, Tick = require('tick-tock')
, one = require('one-time');
/**
* Returns sane defaults about a given value.
*
* @param {String} name Name of property we want.
* @param {Recovery} selfie Recovery instance that got created.
* @param {Object} opts User supplied options we want to check.
* @returns {Number} Some default value.
* @api private
*/
function defaults(name, selfie, opts) {
return millisecond(
name in opts ? opts[name] : (name in selfie ? selfie[name] : Recovery[name])
);
}
/**
* Attempt to recover your connection with reconnection attempt.
*
* @constructor
* @param {Object} options Configuration
* @api public
*/
function Recovery(options) {
var recovery = this;
if (!(recovery instanceof Recovery)) return new Recovery(options);
options = options || {};
recovery.attempt = null; // Stores the current reconnect attempt.
recovery._fn = null; // Stores the callback.
recovery['reconnect timeout'] = defaults('reconnect timeout', recovery, options);
recovery.retries = defaults('retries', recovery, options);
recovery.factor = defaults('factor', recovery, options);
recovery.max = defaults('max', recovery, options);
recovery.min = defaults('min', recovery, options);
recovery.timers = new Tick(recovery);
}
Recovery.prototype = new EventEmitter();
Recovery.prototype.constructor = Recovery;
Recovery['reconnect timeout'] = '30 seconds'; // Maximum time to wait for an answer.
Recovery.max = Infinity; // Maximum delay.
Recovery.min = '500 ms'; // Minimum delay.
Recovery.retries = 10; // Maximum amount of retries.
Recovery.factor = 2; // Exponential back off factor.
/**
* Start a new reconnect procedure.
*
* @returns {Recovery}
* @api public
*/
Recovery.prototype.reconnect = function reconnect() {
var recovery = this;
return recovery.backoff(function backedoff(err, opts) {
opts.duration = (+new Date()) - opts.start;
if (err) return recovery.emit('reconnect failed', err, opts);
recovery.emit('reconnected', opts);
}, recovery.attempt);
};
/**
* Exponential back off algorithm for retry operations. It uses a randomized
* retry so we don't DDOS our server when it goes down under pressure.
*
* @param {Function} fn Callback to be called after the timeout.
* @param {Object} opts Options for configuring the timeout.
* @returns {Recovery}
* @api private
*/
Recovery.prototype.backoff = function backoff(fn, opts) {
var recovery = this;
opts = opts || recovery.attempt || {};
//
// Bailout when we already have a back off process running. We shouldn't call
// the callback then.
//
if (opts.backoff) return recovery;
opts['reconnect timeout'] = defaults('reconnect timeout', recovery, opts);
opts.retries = defaults('retries', recovery, opts);
opts.factor = defaults('factor', recovery, opts);
opts.max = defaults('max', recovery, opts);
opts.min = defaults('min', recovery, opts);
opts.start = +opts.start || +new Date();
opts.duration = +opts.duration || 0;
opts.attempt = +opts.attempt || 0;
//
// Bailout if we are about to make too much attempts.
//
if (opts.attempt === opts.retries) {
fn.call(recovery, new Error('Unable to recover'), opts);
return recovery;
}
//
// Prevent duplicate back off attempts using the same options object and
// increment our attempt as we're about to have another go at this thing.
//
opts.backoff = true;
opts.attempt++;
recovery.attempt = opts;
//
// Calculate the timeout, but make it randomly so we don't retry connections
// at the same interval and defeat the purpose. This exponential back off is
// based on the work of:
//
// http://dthain.blogspot.nl/2009/02/exponential-backoff-in-distributed.html
//
opts.scheduled = opts.attempt !== 1
? Math.min(Math.round(
(Math.random() + 1) * opts.min * Math.pow(opts.factor, opts.attempt - 1)
), opts.max)
: opts.min;
recovery.timers.setTimeout('reconnect', function delay() {
opts.duration = (+new Date()) - opts.start;
opts.backoff = false;
recovery.timers.clear('reconnect, timeout');
//
// Create a `one` function which can only be called once. So we can use the
// same function for different types of invocations to create a much better
// and usable API.
//
var connect = recovery._fn = one(function connect(err) {
recovery.reset();
if (err) return recovery.backoff(fn, opts);
fn.call(recovery, undefined, opts);
});
recovery.emit('reconnect', opts, connect);
recovery.timers.setTimeout('timeout', function timeout() {
var err = new Error('Failed to reconnect in a timely manner');
opts.duration = (+new Date()) - opts.start;
recovery.emit('reconnect timeout', err, opts);
connect(err);
}, opts['reconnect timeout']);
}, opts.scheduled);
//
// Emit a `reconnecting` event with current reconnect options. This allows
// them to update the UI and provide their users with feedback.
//
recovery.emit('reconnect scheduled', opts);
return recovery;
};
/**
* Check if the reconnection process is currently reconnecting.
*
* @returns {Boolean}
* @api public
*/
Recovery.prototype.reconnecting = function reconnecting() {
return !!this.attempt;
};
/**
* Tell our reconnection procedure that we're passed.
*
* @param {Error} err Reconnection failed.
* @returns {Recovery}
* @api public
*/
Recovery.prototype.reconnected = function reconnected(err) {
if (this._fn) this._fn(err);
return this;
};
/**
* Reset the reconnection attempt so it can be re-used again.
*
* @returns {Recovery}
* @api public
*/
Recovery.prototype.reset = function reset() {
this._fn = this.attempt = null;
this.timers.clear('reconnect, timeout');
return this;
};
/**
* Clean up the instance.
*
* @type {Function}
* @returns {Boolean}
* @api public
*/
Recovery.prototype.destroy = destroy('timers attempt _fn');
//
// Expose the module.
//
module.exports = Recovery;
;