oibackoff
Version:
Incremental backoff flow-control for any : fn(function(err, data) { ... });
153 lines (130 loc) • 4.63 kB
JavaScript
// --------------------------------------------------------------------------------------------------------------------
//
// oibackoff.js - backoff functionality for any : fn(function(err, data) { ... });
//
// Copyright (c) 2012 AppsAttic Ltd - http://www.appsattic.com/
// Written by Andrew Chilton <chilts@appsattic.com>
//
// License: http://opensource.org/licenses/MIT
//
// --------------------------------------------------------------------------------------------------------------------
var _ = require('underscore');
var defaults = {
'maxTries' : 3,
'algorithm' : 'exponential',
'delayRatio' : 1, // you could make it any other integer or fraction (e.g. 0.25)
};
// returns 1, 2, 3, 4, 5, 6, 7, ...
function incremental(n) {
return n + 1;
}
// memoizes 0, 1, 2, 4, 8, 16, 32, ...
var exp = [];
function exponential(n) {
if ( exp[n] ) {
return exp[n];
}
if ( n === 0 ) {
exp[0] = 1;
return exp[0];
}
if ( n === 1 ) {
exp[1] = 2;
return exp[1];
}
exp[n] = exponential(n-1) * 2;
return exp[n];
}
// memoizes 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
var fib = [];
function fibonacci(n) {
if ( fib[n] ) {
return fib[n];
}
if ( n <= 1 ) {
fib[n] = n;
return fib[n];
}
fib[n] = fibonacci(n-2) + fibonacci(n-1);
return fib[n];
}
var algorithm = {
fibonacci : fibonacci,
exponential : exponential,
incremental : incremental,
};
var backoff = function(opts) {
// build up all the options
opts = _.extend({}, defaults, opts);
return function() {
if ( arguments.length < 2 ) {
throw 'Provide at least two args : backoff(fn, ..., callback)';
}
// set up a few things we need to keep a check of
var tries = 0;
// the function to call is the first arg, the last is the callback
// an intermediate function can be called on error if provided
var args = Array.prototype.slice.call(arguments);
var fn = args.shift();
var callback = args.pop();
var intermediate = function() {};
if ( _.isFunction( _.last( args ) ) ) {
intermediate = args.pop();
}
var priorErrors = [];
// create the function we want to call when fn() calls back
var myCallback = function(err, data) {
if ( err ) {
// figure out the actual delay using the algorithm, the retry count and the delayRatio
var delay = algorithm[opts.algorithm](tries) * opts.delayRatio;
// call this again but only if we
var doAgain = false;
if ( intermediate(err, tries, delay) === false ) {
// intermediate function has told us not to try again
callback(err, null, priorErrors);
return;
}
if ( opts.maxTries === 0 ) {
doAgain = true;
}
else {
if ( tries < opts.maxTries ) {
doAgain = true;
}
else {
// we've retried enough, call callback as a failure
callback(err, null, priorErrors);
return;
}
}
// remember this error
priorErrors.push(err);
if ( doAgain ) {
// ... and check it isn't over maxDelay
if ( opts.maxDelay && delay > opts.maxDelay ) {
delay = opts.maxDelay;
}
setTimeout(function() {
// increment how many tries we have done
tries++;
// now call it again
fn.apply(null, args);
}, delay * 1000);
}
return;
}
callback(null, data, priorErrors);
};
// add our own callback to the args and call the incoming function
args.push(myCallback);
tries++;
fn.apply(null, args);
};
};
// --------------------------------------------------------------------------------------------------------------------
// exports
exports.exponential = exponential;
exports.incremental = incremental;
exports.fibonacci = fibonacci;
exports.backoff = backoff;
// --------------------------------------------------------------------------------------------------------------------