package/.gitignore0000644000076500000240000000002011716416207014727 0ustar darkangelbgestaff/node_modules/* package/equation.gif0000644000076500000240000000227111716416207015265 0ustar darkangelbgestaffGIF89a1  $ )()),)1011419899<9A@AADAJHJJLJRPRRURZYZZ]Zbabbebjijjmjsqssus{y{{}{,1pH,Ȥrl:ШtJZجxˀ(xL.gan <51zD/w< 5[:,R6C3o&H>1 - %> 8I7v24J š!Hڄ (http://debuggable.com/)", "name": "retry", "description": "Abstraction for exponential and custom retry strategies for failed operations.", "version": "0.6.0", "homepage": "https://github.com/tim-kos/node-retry", "repository": { "type": "git", "url": "git://github.com/felixge/node-retry.git" }, "directories": { "lib": "./lib" }, "main": "index", "engines": { "node": "*" }, "dependencies": {}, "devDependencies": { "fake": "0.2.0", "far": "0.0.1" } }package/Readme.md0000644000076500000240000001270011716416207014466 0ustar darkangelbgestaff# retry Abstraction for exponential and custom retry strategies for failed operations. ## Installation npm install retry ## Current Status This module has been tested and is ready to be used. ## Tutorial The example below will retry a potentially failing `dns.resolve` operation `10` times using an exponential backoff strategy. With the default settings, this means the last attempt is made after `34 minutes and 7 seconds`. ``` javascript var dns = require('dns'); var retry = require('retry'); function faultTolerantResolve(address, cb) { var operation = retry.operation(); operation.attempt(function(currentAttempt) { dns.resolve(address, function(err, addresses) { if (operation.retry(err)) { return; } cb(operation.mainError(), addresses); }); }); } faultTolerantResolve('nodejs.org', function(err, addresses) { console.log(err, addresses); }); ``` Of course you can also configure the factors that go into the exponential backoff. See the API documentation below for all available settings. currentAttempt is an int representing the number of attempts so far. ``` javascript var operation = retry.operation({ retries: 5, factor: 3, minTimeout: 1 * 1000, maxTimeout: 60 * 1000, randomize: true, }); ``` ## API ### retry.operation([options]) Creates a new `RetryOperation` object. See the `retry.timeouts()` function below for available `options`. ### retry.timeouts([options]) Returns an array of timeouts. All time `options` and return values are in milliseconds. If `options` is an array, a copy of that array is returned. `options` is a JS object that can contain any of the following keys: * `retries`: The maximum amount of times to retry the operation. Default is `10`. * `factor`: The exponential factor to use. Default is `2`. * `minTimeout`: The amount of time before starting the first retry. Default is `1000`. * `maxTimeout`: The maximum amount of time between two retries. Default is `Infinity`. * `randomize`: Randomizes the timeouts by multiplying with a factor between `1` to `2`. Default is `false`. The formula used to calculate the individual timeouts is: ``` var Math.min(random * minTimeout * Math.pow(factor, attempt), maxTimeout); ``` Have a look at [this article][article] for a better explanation of approach. If you want to tune your `factor` / `times` settings to attempt the last retry after a certain amount of time, you can use wolfram alpha. For example in order to tune for `10` attempts in `5 minutes`, you can use this equation: ![screenshot](https://github.com/tim-kos/node-retry/raw/master/equation.gif) Explaining the various values from left to right: * `k = 0 ... 9`: The `retries` value (10) * `1000`: The `minTimeout` value in ms (1000) * `x^k`: No need to change this, `x` will be your resulting factor * `5 * 60 * 1000`: The desired total amount of time for retrying in ms (5 minutes) To make this a little easier for you, use wolfram alpha to do the calculations: [http://www.wolframalpha.com/input/?i=Sum%5B1000*x^k%2C+{k%2C+0%2C+9}%5D+%3D+5+*+60+*+1000]() [article]: http://dthain.blogspot.com/2009/02/exponential-backoff-in-distributed.html ### new RetryOperation(timeouts) Creates a new `RetryOperation` where `timeouts` is an array where each value is a timeout given in milliseconds. #### retryOperation.errors() Returns an array of all errors that have been passed to `retryOperation.retry()` so far. #### retryOperation.mainError() A reference to the error object that occured most frequently. Errors are compared using the `error.message` property. If multiple error messages occured the same amount of time, the last error object with that message is returned. If no errors occured so far, the value is `null`. #### retryOperation.attempt(fn, timeoutOps) Defines the function `fn` that is to be retried and executes it for the first time right away. The `fn` function can receive an optional `currentAttempt` callback that represents the number of attempts to execute `fn` so far. Optionally defines `timeoutOps` which is an object having a property `timeout` in miliseconds and a property `cb` callback function. Whenever your retry operation takes longer than `timeout` to execute, the timeout callback function `cb` is called. #### retryOperation.try(fn) This is an alias for `retryOperation.attempt(fn)`. This is deprecated. #### retryOperation.start(fn) This is an alias for `retryOperation.attempt(fn)`. This is deprecated. #### retryOperation.retry(error) Returns `false` when no `error` value is given, or the maximum amount of retries has been reached. Otherwise it returns `true`, and retries the operation after the timeout for the current attempt number. #### retryOperation.attempts() Returns an int representing the number of attempts it took to call `fn` before it was successful. ## License retry is licensed under the MIT license. #Changelog 0.6.0 Introduced optional timeOps parameter for the attempt() function which is an object having a property timeout in miliseconds and a property cb callback function. Whenever your retry operation takes longer than timeout to execute, the timeout callback function cb is called. 0.5.0 Some minor refactorings. 0.4.0 Changed retryOperation.try() to retryOperation.attempt(). Deprecated the aliases start() and try() for it. 0.3.0 Added retryOperation.start() which is an alias for retryOperation.try(). 0.2.0 Added attempts() function and parameter to retryOperation.try() representing the number of attempts it took to call fn(). package/example/dns.js0000644000076500000240000000125511716416207015527 0ustar darkangelbgestaffvar dns = require('dns'); var retry = require('../lib/retry'); function faultTolerantResolve(address, cb) { var opts = { times: 2, factor: 2, minTimeout: 1 * 1000, maxTimeout: 2 * 1000, randomize: true }; var operation = retry.operation(opts); operation.attempt(function(currentAttempt) { dns.resolve(address, function(err, addresses) { if (operation.retry(err)) { return; } cb(operation.mainError(), operation.errors(), addresses); }); }); } faultTolerantResolve('nodejs.org', function(err, errors, addresses) { console.warn('err:'); console.log(err); console.warn('addresses:'); console.log(addresses); });package/lib/retry_operation.js0000644000076500000240000000433311716416207017303 0ustar darkangelbgestafffunction RetryOperation(timeouts) { this._timeouts = timeouts; this._fn = null; this._errors = []; this._attempts = 1; this._operationTimeout = null; this._operationTimeoutCb = null; this._timeout = null; } module.exports = RetryOperation; RetryOperation.prototype.retry = function(err) { if (this._timeout) { clearTimeout(this._timeout); } if (!err) { return false; } this._errors.push(err); var timeout = this._timeouts.shift(); if (timeout === undefined) { return false; } this._attempts++; var self = this; setTimeout(function() { self._fn(self._attempts); if (self._operationTimeoutCb) { self._timeout = setTimeout(function() { self._operationTimeoutCb(self._attempts); }, self._operationTimeout); } }, timeout); return true; }; RetryOperation.prototype.attempt = function(fn, timeoutOps) { this._fn = fn; if (timeoutOps) { if (timeoutOps.timeout) { this._operationTimeout = timeoutOps.timeout; } if (timeoutOps.cb) { this._operationTimeoutCb = timeoutOps.cb; } } this._fn(this._attempts); var self = this; if (this._operationTimeoutCb) { this._timeout = setTimeout(function() { self._operationTimeoutCb(); }, self._operationTimeout); } }; RetryOperation.prototype.try = function(fn) { console.log('Using RetryOperation.try() is deprecated'); this.attempt(fn); }; RetryOperation.prototype.start = function(fn) { console.log('Using RetryOperation.start() is deprecated'); this.attempt(fn); }; RetryOperation.prototype.start = RetryOperation.prototype.try; RetryOperation.prototype.errors = function() { return this._errors; }; RetryOperation.prototype.attempts = function() { return this._attempts; }; RetryOperation.prototype.mainError = function() { if (this._errors.length === 0) { return null; } var counts = {}; var mainError = null; var mainErrorCount = 0; for (var i = 0; i < this._errors.length; i++) { var error = this._errors[i]; var message = error.message; var count = (counts[message] || 0) + 1; counts[message] = count; if (count >= mainErrorCount) { mainError = error; mainErrorCount = count; } } return mainError; };package/lib/retry.js0000644000076500000240000000211611716416207015220 0ustar darkangelbgestaffvar RetryOperation = require('./retry_operation'); exports.operation = function(options) { var timeouts = exports.timeouts(options); return new RetryOperation(timeouts); }; exports.timeouts = function(options) { if (options instanceof Array) { return [].concat(options); } var opts = { retries: 10, factor: 2, minTimeout: 1 * 1000, maxTimeout: Infinity, randomize: false }; for (var key in options) { opts[key] = options[key]; } if (opts.minTimeout > opts.maxTimeout) { throw new Error('minTimeout is greater than maxTimeout'); } var timeouts = []; for (var i = 0; i < opts.retries; i++) { timeouts.push(this._createTimeout(i, opts)); } // sort the array numerically ascending timeouts.sort(function(a,b) { return a - b; }); return timeouts; }; exports._createTimeout = function(attempt, opts) { var random = (opts.randomize) ? (Math.random() + 1) : 1; var timeout = Math.round(random * opts.minTimeout * Math.pow(opts.factor, attempt)); timeout = Math.min(timeout, opts.maxTimeout); return timeout; };package/test/common.js0000644000076500000240000000032011716416207015547 0ustar darkangelbgestaffvar common = module.exports; var path = require('path'); var rootDir = path.join(__dirname, '..'); common.dir = { lib: rootDir + '/lib' }; common.assert = require('assert'); common.fake = require('fake');package/test/runner.js0000644000076500000240000000014611716416207015576 0ustar darkangelbgestaffvar far = require('far').create(); far.add(__dirname); far.include(/\/test-.*\.js$/); far.execute(); package/test/integration/test-retry-operation.js0000644000076500000240000000405311716416207022731 0ustar darkangelbgestaffvar common = require('../common'); var assert = common.assert; var fake = common.fake.create(); var retry = require(common.dir.lib + '/retry'); (function testErrors() { var operation = retry.operation(); var error = new Error('some error'); var error2 = new Error('some other error'); operation._errors.push(error); operation._errors.push(error2); assert.deepEqual(operation.errors(), [error, error2]); })(); (function testMainErrorReturnsMostFrequentError() { var operation = retry.operation(); var error = new Error('some error'); var error2 = new Error('some other error'); operation._errors.push(error); operation._errors.push(error2); operation._errors.push(error); assert.strictEqual(operation.mainError(), error); })(); (function testMainErrorReturnsLastErrorOnEqualCount() { var operation = retry.operation(); var error = new Error('some error'); var error2 = new Error('some other error'); operation._errors.push(error); operation._errors.push(error2); assert.strictEqual(operation.mainError(), error2); })(); (function testAttempt() { var operation = retry.operation(); var fn = new Function(); var timeoutOpts = { timeout: 1, cb: function() {} }; operation.attempt(fn, timeoutOpts); assert.strictEqual(fn, operation._fn); assert.strictEqual(timeoutOpts.timeout, operation._operationTimeout); assert.strictEqual(timeoutOpts.cb, operation._operationTimeoutCb); })(); (function testRetry() { var times = 3; var error = new Error('some error'); var operation = retry.operation([1, 2, 3]); var attempts = 0; var finalCallback = fake.callback('finalCallback'); fake.expectAnytime(finalCallback); var fn = function() { operation.attempt(function(currentAttempt) { attempts++; assert.equal(currentAttempt, attempts); if (operation.retry(error)) { return; } assert.strictEqual(attempts, 4); assert.strictEqual(operation.attempts(), attempts); assert.strictEqual(operation.mainError(), error); finalCallback(); }); }; fn(); })();package/test/integration/test-timeouts.js0000644000076500000240000000336311716416207021442 0ustar darkangelbgestaffvar common = require('../common'); var assert = common.assert; var retry = require(common.dir.lib + '/retry'); (function testDefaultValues() { var timeouts = retry.timeouts(); assert.equal(timeouts.length, 10); assert.equal(timeouts[0], 1000); assert.equal(timeouts[1], 2000); assert.equal(timeouts[2], 4000); })(); (function testDefaultValuesWithRandomize() { var minTimeout = 5000; var timeouts = retry.timeouts({ minTimeout: minTimeout, randomize: true }); assert.equal(timeouts.length, 10); assert.ok(timeouts[0] > minTimeout); assert.ok(timeouts[1] > timeouts[0]); assert.ok(timeouts[2] > timeouts[1]); })(); (function testPassedTimeoutsAreUsed() { var timeoutsArray = [1000, 2000, 3000]; var timeouts = retry.timeouts(timeoutsArray); assert.deepEqual(timeouts, timeoutsArray); assert.notStrictEqual(timeouts, timeoutsArray); })(); (function testTimeoutsAreWithinBoundaries() { var minTimeout = 1000; var maxTimeout = 10000; var timeouts = retry.timeouts({ minTimeout: minTimeout, maxTimeout: maxTimeout }); for (var i = 0; i < timeouts; i++) { assert.ok(timeouts[i] >= minTimeout); assert.ok(timeouts[i] <= maxTimeout); } })(); (function testTimeoutsAreIncremental() { var timeouts = retry.timeouts(); var lastTimeout = timeouts[0]; for (var i = 0; i < timeouts; i++) { assert.ok(timeouts[i] > lastTimeout); lastTimeout = timeouts[i]; } })(); (function testTimeoutsAreIncrementalForFactorsLessThanOne() { var timeouts = retry.timeouts({ retries: 3, factor: 0.5 }); var expected = [250, 500, 1000]; assert.deepEqual(expected, timeouts); })(); (function testRetries() { var timeouts = retry.timeouts({retries: 2}); assert.strictEqual(timeouts.length, 2); })();