UNPKG

recovery

Version:

Recover from a network failure using randomized exponential backoff

221 lines (184 loc) 6.25 kB
'use strict'; 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;