├── .github └── workflows │ └── test-node.yml ├── .gitignore ├── License ├── Makefile ├── README.md ├── equation.gif ├── example ├── dns.js └── stop.js ├── index.js ├── lib ├── retry.js └── retry_operation.js ├── package.json └── test ├── common.js └── integration ├── test-forever.js ├── test-retry-operation.js ├── test-retry-wrap.js └── test-timeouts.js /.github/workflows/test-node.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | jobs: 11 | build: 12 | strategy: 13 | matrix: 14 | node-version: [12] 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm install 22 | - run: npm run test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/* 2 | npm-debug.log 3 | coverage 4 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011: 2 | Tim Koschützki (tim@debuggable.com) 3 | Felix Geisendörfer (felix@debuggable.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | release-major: test 4 | npm version major -m "Release %s" 5 | git push 6 | npm publish 7 | 8 | release-minor: test 9 | npm version minor -m "Release %s" 10 | git push 11 | npm publish 12 | 13 | release-patch: test 14 | npm version patch -m "Release %s" 15 | git push 16 | npm publish 17 | 18 | .PHONY: test release-major release-minor release-patch 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Build Status](https://secure.travis-ci.org/tim-kos/node-retry.svg?branch=master)](http://travis-ci.org/tim-kos/node-retry "Check this project's build status on TravisCI") 3 | [![codecov](https://codecov.io/gh/tim-kos/node-retry/branch/master/graph/badge.svg)](https://codecov.io/gh/tim-kos/node-retry) 4 | 5 | 6 | # retry 7 | 8 | Abstraction for exponential and custom retry strategies for failed operations. 9 | 10 | ## Installation 11 | 12 | npm install retry 13 | 14 | ## Current Status 15 | 16 | This module has been tested and is ready to be used. 17 | 18 | ## Tutorial 19 | 20 | The example below will retry a potentially failing `dns.resolve` operation 21 | `10` times using an exponential backoff strategy. With the default settings, this 22 | means the last attempt is made after `17 minutes and 3 seconds`. 23 | 24 | ``` javascript 25 | var dns = require('dns'); 26 | var retry = require('retry'); 27 | 28 | function faultTolerantResolve(address, cb) { 29 | var operation = retry.operation(); 30 | 31 | operation.attempt(function(currentAttempt) { 32 | dns.resolve(address, function(err, addresses) { 33 | if (operation.retry(err)) { 34 | return; 35 | } 36 | 37 | cb(err ? operation.mainError() : null, addresses); 38 | }); 39 | }); 40 | } 41 | 42 | faultTolerantResolve('nodejs.org', function(err, addresses) { 43 | console.log(err, addresses); 44 | }); 45 | ``` 46 | 47 | Of course you can also configure the factors that go into the exponential 48 | backoff. See the API documentation below for all available settings. 49 | currentAttempt is an int representing the number of attempts so far. 50 | 51 | ``` javascript 52 | var operation = retry.operation({ 53 | retries: 5, 54 | factor: 3, 55 | minTimeout: 1 * 1000, 56 | maxTimeout: 60 * 1000, 57 | randomize: true, 58 | }); 59 | ``` 60 | 61 | ### Example with promises 62 | 63 | ```javascript 64 | const retry = require('retry') 65 | const delay = require('delay') 66 | 67 | const isItGood = [false, false, true] 68 | let numAttempt = 0 69 | 70 | function retryer() { 71 | let operation = retry.operation() 72 | 73 | return new Promise((resolve, reject) => { 74 | operation.attempt(async currentAttempt => { 75 | console.log('Attempt #:', numAttempt) 76 | await delay(2000) 77 | 78 | const err = !isItGood[numAttempt] ? true : null 79 | if (operation.retry(err)) { 80 | numAttempt++ 81 | return 82 | } 83 | 84 | if (isItGood[numAttempt]) { 85 | resolve('All good!') 86 | } else { 87 | reject(operation.mainError()) 88 | } 89 | }) 90 | }) 91 | } 92 | 93 | async function main() { 94 | console.log('Start') 95 | await retryer() 96 | console.log('End') 97 | } 98 | 99 | main() 100 | ``` 101 | 102 | ## API 103 | 104 | ### retry.operation([options]) 105 | 106 | Creates a new `RetryOperation` object. `options` is the same as `retry.timeouts()`'s `options`, with three additions: 107 | 108 | * `forever`: Whether to retry forever, defaults to `false`. 109 | * `unref`: Whether to [unref](https://nodejs.org/api/timers.html#timers_unref) the setTimeout's, defaults to `false`. 110 | * `maxRetryTime`: The maximum time (in milliseconds) that the retried operation is allowed to run. Default is `Infinity`. 111 | 112 | ### retry.timeouts([options]) 113 | 114 | Returns an array of timeouts. All time `options` and return values are in 115 | milliseconds. If `options` is an array, a copy of that array is returned. 116 | 117 | `options` is a JS object that can contain any of the following keys: 118 | 119 | * `retries`: The maximum amount of times to retry the operation. Default is `10`. Seting this to `1` means `do it once, then retry it once`. 120 | * `factor`: The exponential factor to use. Default is `2`. 121 | * `minTimeout`: The number of milliseconds before starting the first retry. Default is `1000`. 122 | * `maxTimeout`: The maximum number of milliseconds between two retries. Default is `Infinity`. 123 | * `randomize`: Randomizes the timeouts by multiplying with a factor between `1` to `2`. Default is `false`. 124 | 125 | The formula used to calculate the individual timeouts is: 126 | 127 | ``` 128 | Math.min(random * minTimeout * Math.pow(factor, attempt), maxTimeout) 129 | ``` 130 | 131 | Have a look at [this article][article] for a better explanation of approach. 132 | 133 | If you want to tune your `factor` / `times` settings to attempt the last retry 134 | after a certain amount of time, you can use wolfram alpha. For example in order 135 | to tune for `10` attempts in `5 minutes`, you can use this equation: 136 | 137 | ![screenshot](https://github.com/tim-kos/node-retry/raw/master/equation.gif) 138 | 139 | Explaining the various values from left to right: 140 | 141 | * `k = 0 ... 9`: The `retries` value (10) 142 | * `1000`: The `minTimeout` value in ms (1000) 143 | * `x^k`: No need to change this, `x` will be your resulting factor 144 | * `5 * 60 * 1000`: The desired total amount of time for retrying in ms (5 minutes) 145 | 146 | To make this a little easier for you, use wolfram alpha to do the calculations: 147 | 148 | 149 | 150 | [article]: http://dthain.blogspot.com/2009/02/exponential-backoff-in-distributed.html 151 | 152 | ### retry.createTimeout(attempt, opts) 153 | 154 | Returns a new `timeout` (integer in milliseconds) based on the given parameters. 155 | 156 | `attempt` is an integer representing for which retry the timeout should be calculated. If your retry operation was executed 4 times you had one attempt and 3 retries. If you then want to calculate a new timeout, you should set `attempt` to 4 (attempts are zero-indexed). 157 | 158 | `opts` can include `factor`, `minTimeout`, `randomize` (boolean) and `maxTimeout`. They are documented above. 159 | 160 | `retry.createTimeout()` is used internally by `retry.timeouts()` and is public for you to be able to create your own timeouts for reinserting an item, see [issue #13](https://github.com/tim-kos/node-retry/issues/13). 161 | 162 | ### retry.wrap(obj, [options], [methodNames]) 163 | 164 | Wrap all functions of the `obj` with retry. Optionally you can pass operation options and 165 | an array of method names which need to be wrapped. 166 | 167 | ``` 168 | retry.wrap(obj) 169 | 170 | retry.wrap(obj, ['method1', 'method2']) 171 | 172 | retry.wrap(obj, {retries: 3}) 173 | 174 | retry.wrap(obj, {retries: 3}, ['method1', 'method2']) 175 | ``` 176 | The `options` object can take any options that the usual call to `retry.operation` can take. 177 | 178 | ### new RetryOperation(timeouts, [options]) 179 | 180 | Creates a new `RetryOperation` where `timeouts` is an array where each value is 181 | a timeout given in milliseconds. 182 | 183 | Available options: 184 | * `forever`: Whether to retry forever, defaults to `false`. 185 | * `unref`: Wether to [unref](https://nodejs.org/api/timers.html#timers_unref) the setTimeout's, defaults to `false`. 186 | 187 | If `forever` is true, the following changes happen: 188 | * `RetryOperation.errors()` will only output an array of one item: the last error. 189 | * `RetryOperation` will repeatedly use the `timeouts` array. Once all of its timeouts have been used up, it restarts with the first timeout, then uses the second and so on. 190 | 191 | #### retryOperation.errors() 192 | 193 | Returns an array of all errors that have been passed to `retryOperation.retry()` so far. The 194 | returning array has the errors ordered chronologically based on when they were passed to 195 | `retryOperation.retry()`, which means the first passed error is at index zero and the last is 196 | at the last index. 197 | 198 | #### retryOperation.mainError() 199 | 200 | A reference to the error object that occured most frequently. Errors are 201 | compared using the `error.message` property. 202 | 203 | If multiple error messages occured the same amount of time, the last error 204 | object with that message is returned. 205 | 206 | If no errors occured so far, the value is `null`. 207 | 208 | #### retryOperation.attempt(fn, timeoutOps) 209 | 210 | Defines the function `fn` that is to be retried and executes it for the first 211 | time right away. The `fn` function can receive an optional `currentAttempt` callback that represents the number of attempts to execute `fn` so far. 212 | 213 | Optionally defines `timeoutOps` which is an object having a property `timeout` in miliseconds and a property `cb` callback function. 214 | Whenever your retry operation takes longer than `timeout` to execute, the timeout callback function `cb` is called. 215 | 216 | 217 | #### retryOperation.try(fn) 218 | 219 | This is an alias for `retryOperation.attempt(fn)`. This is deprecated. Please use `retryOperation.attempt(fn)` instead. 220 | 221 | #### retryOperation.start(fn) 222 | 223 | This is an alias for `retryOperation.attempt(fn)`. This is deprecated. Please use `retryOperation.attempt(fn)` instead. 224 | 225 | #### retryOperation.retry(error) 226 | 227 | Returns `false` when no `error` value is given, or the maximum amount of retries 228 | has been reached. 229 | 230 | Otherwise it returns `true`, and retries the operation after the timeout for 231 | the current attempt number. 232 | 233 | #### retryOperation.stop() 234 | 235 | Allows you to stop the operation being retried. Useful for aborting the operation on a fatal error etc. 236 | 237 | #### retryOperation.reset() 238 | 239 | Resets the internal state of the operation object, so that you can call `attempt()` again as if this was a new operation object. 240 | 241 | #### retryOperation.attempts() 242 | 243 | Returns an int representing the number of attempts it took to call `fn` before it was successful. 244 | 245 | ## License 246 | 247 | retry is licensed under the MIT license. 248 | 249 | 250 | # Changelog 251 | 252 | 0.10.0 Adding `stop` functionality, thanks to @maxnachlinger. 253 | 254 | 0.9.0 Adding `unref` functionality, thanks to @satazor. 255 | 256 | 0.8.0 Implementing retry.wrap. 257 | 258 | 0.7.0 Some bug fixes and made retry.createTimeout() public. Fixed issues [#10](https://github.com/tim-kos/node-retry/issues/10), [#12](https://github.com/tim-kos/node-retry/issues/12), and [#13](https://github.com/tim-kos/node-retry/issues/13). 259 | 260 | 0.6.0 Introduced optional timeOps parameter for the attempt() function which is an object having a property timeout in milliseconds and a property cb callback function. Whenever your retry operation takes longer than timeout to execute, the timeout callback function cb is called. 261 | 262 | 0.5.0 Some minor refactoring. 263 | 264 | 0.4.0 Changed retryOperation.try() to retryOperation.attempt(). Deprecated the aliases start() and try() for it. 265 | 266 | 0.3.0 Added retryOperation.start() which is an alias for retryOperation.try(). 267 | 268 | 0.2.0 Added attempts() function and parameter to retryOperation.try() representing the number of attempts it took to call fn(). 269 | -------------------------------------------------------------------------------- /equation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-kos/node-retry/11efd6e4e896e06b7873df4f6e187c1e6dd2cf1b/equation.gif -------------------------------------------------------------------------------- /example/dns.js: -------------------------------------------------------------------------------- 1 | var dns = require('dns'); 2 | var retry = require('../lib/retry'); 3 | 4 | function faultTolerantResolve(address, cb) { 5 | var opts = { 6 | retries: 2, 7 | factor: 2, 8 | minTimeout: 1 * 1000, 9 | maxTimeout: 2 * 1000, 10 | randomize: true 11 | }; 12 | var operation = retry.operation(opts); 13 | 14 | operation.attempt(function(currentAttempt) { 15 | dns.resolve(address, function(err, addresses) { 16 | if (operation.retry(err)) { 17 | return; 18 | } 19 | 20 | cb(operation.mainError(), operation.errors(), addresses); 21 | }); 22 | }); 23 | } 24 | 25 | faultTolerantResolve('nodejs.org', function(err, errors, addresses) { 26 | console.warn('err:'); 27 | console.log(err); 28 | 29 | console.warn('addresses:'); 30 | console.log(addresses); 31 | }); -------------------------------------------------------------------------------- /example/stop.js: -------------------------------------------------------------------------------- 1 | var retry = require('../lib/retry'); 2 | 3 | function attemptAsyncOperation(someInput, cb) { 4 | var opts = { 5 | retries: 2, 6 | factor: 2, 7 | minTimeout: 1 * 1000, 8 | maxTimeout: 2 * 1000, 9 | randomize: true 10 | }; 11 | var operation = retry.operation(opts); 12 | 13 | operation.attempt(function(currentAttempt) { 14 | failingAsyncOperation(someInput, function(err, result) { 15 | 16 | if (err && err.message === 'A fatal error') { 17 | operation.stop(); 18 | return cb(err); 19 | } 20 | 21 | if (operation.retry(err)) { 22 | return; 23 | } 24 | 25 | cb(operation.mainError(), operation.errors(), result); 26 | }); 27 | }); 28 | } 29 | 30 | attemptAsyncOperation('test input', function(err, errors, result) { 31 | console.warn('err:'); 32 | console.log(err); 33 | 34 | console.warn('result:'); 35 | console.log(result); 36 | }); 37 | 38 | function failingAsyncOperation(input, cb) { 39 | return setImmediate(cb.bind(null, new Error('A fatal error'))); 40 | } 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/retry'); -------------------------------------------------------------------------------- /lib/retry.js: -------------------------------------------------------------------------------- 1 | var RetryOperation = require('./retry_operation'); 2 | 3 | exports.operation = function(options) { 4 | var timeouts = exports.timeouts(options); 5 | return new RetryOperation(timeouts, { 6 | forever: options && (options.forever || options.retries === Infinity), 7 | unref: options && options.unref, 8 | maxRetryTime: options && options.maxRetryTime 9 | }); 10 | }; 11 | 12 | exports.timeouts = function(options) { 13 | if (options instanceof Array) { 14 | return [].concat(options); 15 | } 16 | 17 | var opts = { 18 | retries: 10, 19 | factor: 2, 20 | minTimeout: 1 * 1000, 21 | maxTimeout: Infinity, 22 | randomize: false 23 | }; 24 | for (var key in options) { 25 | opts[key] = options[key]; 26 | } 27 | 28 | if (opts.minTimeout > opts.maxTimeout) { 29 | throw new Error('minTimeout is greater than maxTimeout'); 30 | } 31 | 32 | var timeouts = []; 33 | for (var i = 0; i < opts.retries; i++) { 34 | timeouts.push(this.createTimeout(i, opts)); 35 | } 36 | 37 | if (options && options.forever && !timeouts.length) { 38 | timeouts.push(this.createTimeout(i, opts)); 39 | } 40 | 41 | // sort the array numerically ascending 42 | timeouts.sort(function(a,b) { 43 | return a - b; 44 | }); 45 | 46 | return timeouts; 47 | }; 48 | 49 | exports.createTimeout = function(attempt, opts) { 50 | var random = (opts.randomize) 51 | ? (Math.random() + 1) 52 | : 1; 53 | 54 | var timeout = Math.round(random * Math.max(opts.minTimeout, 1) * Math.pow(opts.factor, attempt)); 55 | timeout = Math.min(timeout, opts.maxTimeout); 56 | 57 | return timeout; 58 | }; 59 | 60 | exports.wrap = function(obj, options, methods) { 61 | if (options instanceof Array) { 62 | methods = options; 63 | options = null; 64 | } 65 | 66 | if (!methods) { 67 | methods = []; 68 | for (var key in obj) { 69 | if (typeof obj[key] === 'function') { 70 | methods.push(key); 71 | } 72 | } 73 | } 74 | 75 | for (var i = 0; i < methods.length; i++) { 76 | var method = methods[i]; 77 | var original = obj[method]; 78 | 79 | obj[method] = function retryWrapper(original) { 80 | var op = exports.operation(options); 81 | var args = Array.prototype.slice.call(arguments, 1); 82 | var callback = args.pop(); 83 | 84 | args.push(function(err) { 85 | if (op.retry(err)) { 86 | return; 87 | } 88 | if (err) { 89 | arguments[0] = op.mainError(); 90 | } 91 | callback.apply(this, arguments); 92 | }); 93 | 94 | op.attempt(function() { 95 | original.apply(obj, args); 96 | }); 97 | }.bind(obj, original); 98 | obj[method].options = options; 99 | } 100 | }; 101 | -------------------------------------------------------------------------------- /lib/retry_operation.js: -------------------------------------------------------------------------------- 1 | function RetryOperation(timeouts, options) { 2 | // Compatibility for the old (timeouts, retryForever) signature 3 | if (typeof options === 'boolean') { 4 | options = { forever: options }; 5 | } 6 | 7 | this._originalTimeouts = JSON.parse(JSON.stringify(timeouts)); 8 | this._timeouts = timeouts; 9 | this._options = options || {}; 10 | this._maxRetryTime = options && options.maxRetryTime || Infinity; 11 | this._fn = null; 12 | this._errors = []; 13 | this._attempts = 1; 14 | this._operationTimeout = null; 15 | this._operationTimeoutCb = null; 16 | this._timeout = null; 17 | this._operationStart = null; 18 | this._timer = null; 19 | 20 | if (this._options.forever) { 21 | this._cachedTimeouts = this._timeouts.slice(0); 22 | } 23 | } 24 | module.exports = RetryOperation; 25 | 26 | RetryOperation.prototype.reset = function() { 27 | this._attempts = 1; 28 | this._timeouts = this._originalTimeouts.slice(0); 29 | } 30 | 31 | RetryOperation.prototype.stop = function() { 32 | if (this._timeout) { 33 | clearTimeout(this._timeout); 34 | } 35 | if (this._timer) { 36 | clearTimeout(this._timer); 37 | } 38 | 39 | this._timeouts = []; 40 | this._cachedTimeouts = null; 41 | }; 42 | 43 | RetryOperation.prototype.retry = function(err) { 44 | if (this._timeout) { 45 | clearTimeout(this._timeout); 46 | } 47 | 48 | if (!err) { 49 | return false; 50 | } 51 | var currentTime = new Date().getTime(); 52 | if (err && currentTime - this._operationStart >= this._maxRetryTime) { 53 | this._errors.push(err); 54 | this._errors.unshift(new Error('RetryOperation timeout occurred')); 55 | return false; 56 | } 57 | 58 | this._errors.push(err); 59 | 60 | var timeout = this._timeouts.shift(); 61 | if (timeout === undefined) { 62 | if (this._cachedTimeouts) { 63 | // retry forever, only keep last error 64 | this._errors.splice(0, this._errors.length - 1); 65 | timeout = this._cachedTimeouts.slice(-1); 66 | } else { 67 | return false; 68 | } 69 | } 70 | 71 | var self = this; 72 | this._timer = setTimeout(function() { 73 | self._attempts++; 74 | 75 | if (self._operationTimeoutCb) { 76 | self._timeout = setTimeout(function() { 77 | self._operationTimeoutCb(self._attempts); 78 | }, self._operationTimeout); 79 | 80 | if (self._options.unref) { 81 | self._timeout.unref(); 82 | } 83 | } 84 | 85 | self._fn(self._attempts); 86 | }, timeout); 87 | 88 | if (this._options.unref) { 89 | this._timer.unref(); 90 | } 91 | 92 | return true; 93 | }; 94 | 95 | RetryOperation.prototype.attempt = function(fn, timeoutOps) { 96 | this._fn = fn; 97 | 98 | if (timeoutOps) { 99 | if (timeoutOps.timeout) { 100 | this._operationTimeout = timeoutOps.timeout; 101 | } 102 | if (timeoutOps.cb) { 103 | this._operationTimeoutCb = timeoutOps.cb; 104 | } 105 | } 106 | 107 | var self = this; 108 | if (this._operationTimeoutCb) { 109 | this._timeout = setTimeout(function() { 110 | self._operationTimeoutCb(); 111 | }, self._operationTimeout); 112 | } 113 | 114 | this._operationStart = new Date().getTime(); 115 | 116 | this._fn(this._attempts); 117 | }; 118 | 119 | RetryOperation.prototype.try = function(fn) { 120 | console.log('Using RetryOperation.try() is deprecated'); 121 | this.attempt(fn); 122 | }; 123 | 124 | RetryOperation.prototype.start = function(fn) { 125 | console.log('Using RetryOperation.start() is deprecated'); 126 | this.attempt(fn); 127 | }; 128 | 129 | RetryOperation.prototype.start = RetryOperation.prototype.try; 130 | 131 | RetryOperation.prototype.errors = function() { 132 | return this._errors; 133 | }; 134 | 135 | RetryOperation.prototype.attempts = function() { 136 | return this._attempts; 137 | }; 138 | 139 | RetryOperation.prototype.mainError = function() { 140 | if (this._errors.length === 0) { 141 | return null; 142 | } 143 | 144 | var counts = {}; 145 | var mainError = null; 146 | var mainErrorCount = 0; 147 | 148 | for (var i = 0; i < this._errors.length; i++) { 149 | var error = this._errors[i]; 150 | var message = error.message; 151 | var count = (counts[message] || 0) + 1; 152 | 153 | counts[message] = count; 154 | 155 | if (count >= mainErrorCount) { 156 | mainError = error; 157 | mainErrorCount = count; 158 | } 159 | } 160 | 161 | return mainError; 162 | }; 163 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Tim Koschützki (http://debuggable.com/)", 3 | "name": "retry", 4 | "description": "Abstraction for exponential and custom retry strategies for failed operations.", 5 | "keywords": "retry, exponential backoff, auto-retry, multiple attempts, custom retry", 6 | "license": "MIT", 7 | "version": "0.13.1", 8 | "homepage": "https://github.com/tim-kos/node-retry", 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/tim-kos/node-retry.git" 12 | }, 13 | "files": [ 14 | "lib", 15 | "example" 16 | ], 17 | "directories": { 18 | "lib": "./lib" 19 | }, 20 | "main": "index.js", 21 | "engines": { 22 | "node": ">= 4" 23 | }, 24 | "dependencies": {}, 25 | "devDependencies": { 26 | "fake": "0.2.0", 27 | "istanbul": "^0.4.5", 28 | "tape": "^4.8.0" 29 | }, 30 | "scripts": { 31 | "test": "./node_modules/.bin/istanbul cover ./node_modules/tape/bin/tape ./test/integration/*.js", 32 | "release:major": "env SEMANTIC=major npm run release", 33 | "release:minor": "env SEMANTIC=minor npm run release", 34 | "release:patch": "env SEMANTIC=patch npm run release", 35 | "release": "npm version ${SEMANTIC:-patch} -m \"Release %s\" && git push && git push --tags && npm publish" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | var common = module.exports; 2 | var path = require('path'); 3 | 4 | var rootDir = path.join(__dirname, '..'); 5 | common.dir = { 6 | lib: rootDir + '/lib' 7 | }; 8 | 9 | common.assert = require('assert'); 10 | common.fake = require('fake'); -------------------------------------------------------------------------------- /test/integration/test-forever.js: -------------------------------------------------------------------------------- 1 | var common = require('../common'); 2 | var assert = common.assert; 3 | var retry = require(common.dir.lib + '/retry'); 4 | 5 | (function testForeverUsesFirstTimeout() { 6 | var operation = retry.operation({ 7 | retries: 0, 8 | minTimeout: 100, 9 | maxTimeout: 100, 10 | forever: true 11 | }); 12 | 13 | operation.attempt(function(numAttempt) { 14 | console.log('>numAttempt', numAttempt); 15 | var err = new Error("foo"); 16 | if (numAttempt == 10) { 17 | operation.stop(); 18 | } 19 | 20 | if (operation.retry(err)) { 21 | return; 22 | } 23 | }); 24 | })(); 25 | -------------------------------------------------------------------------------- /test/integration/test-retry-operation.js: -------------------------------------------------------------------------------- 1 | var common = require('../common'); 2 | var assert = common.assert; 3 | var fake = common.fake.create(); 4 | var retry = require(common.dir.lib + '/retry'); 5 | 6 | (function testReset() { 7 | var error = new Error('some error'); 8 | var operation = retry.operation([1, 2, 3]); 9 | var attempts = 0; 10 | 11 | var finalCallback = fake.callback('finalCallback'); 12 | fake.expectAnytime(finalCallback); 13 | 14 | var expectedFinishes = 1; 15 | var finishes = 0; 16 | 17 | var fn = function() { 18 | operation.attempt(function(currentAttempt) { 19 | attempts++; 20 | assert.equal(currentAttempt, attempts); 21 | if (operation.retry(error)) { 22 | return; 23 | } 24 | 25 | finishes++ 26 | assert.equal(expectedFinishes, finishes); 27 | assert.strictEqual(attempts, 4); 28 | assert.strictEqual(operation.attempts(), attempts); 29 | assert.strictEqual(operation.mainError(), error); 30 | 31 | if (finishes < 2) { 32 | attempts = 0; 33 | expectedFinishes++; 34 | operation.reset(); 35 | fn() 36 | } else { 37 | finalCallback(); 38 | } 39 | }); 40 | }; 41 | 42 | fn(); 43 | })(); 44 | 45 | (function testErrors() { 46 | var operation = retry.operation(); 47 | 48 | var error = new Error('some error'); 49 | var error2 = new Error('some other error'); 50 | operation._errors.push(error); 51 | operation._errors.push(error2); 52 | 53 | assert.deepEqual(operation.errors(), [error, error2]); 54 | })(); 55 | 56 | (function testMainErrorReturnsMostFrequentError() { 57 | var operation = retry.operation(); 58 | var error = new Error('some error'); 59 | var error2 = new Error('some other error'); 60 | 61 | operation._errors.push(error); 62 | operation._errors.push(error2); 63 | operation._errors.push(error); 64 | 65 | assert.strictEqual(operation.mainError(), error); 66 | })(); 67 | 68 | (function testMainErrorReturnsLastErrorOnEqualCount() { 69 | var operation = retry.operation(); 70 | var error = new Error('some error'); 71 | var error2 = new Error('some other error'); 72 | 73 | operation._errors.push(error); 74 | operation._errors.push(error2); 75 | 76 | assert.strictEqual(operation.mainError(), error2); 77 | })(); 78 | 79 | (function testAttempt() { 80 | var operation = retry.operation(); 81 | var fn = new Function(); 82 | 83 | var timeoutOpts = { 84 | timeout: 1, 85 | cb: function() {} 86 | }; 87 | operation.attempt(fn, timeoutOpts); 88 | 89 | assert.strictEqual(fn, operation._fn); 90 | assert.strictEqual(timeoutOpts.timeout, operation._operationTimeout); 91 | assert.strictEqual(timeoutOpts.cb, operation._operationTimeoutCb); 92 | })(); 93 | 94 | (function testRetry() { 95 | var error = new Error('some error'); 96 | var operation = retry.operation([1, 2, 3]); 97 | var attempts = 0; 98 | 99 | var finalCallback = fake.callback('finalCallback'); 100 | fake.expectAnytime(finalCallback); 101 | 102 | var fn = function() { 103 | operation.attempt(function(currentAttempt) { 104 | attempts++; 105 | assert.equal(currentAttempt, attempts); 106 | if (operation.retry(error)) { 107 | return; 108 | } 109 | 110 | assert.strictEqual(attempts, 4); 111 | assert.strictEqual(operation.attempts(), attempts); 112 | assert.strictEqual(operation.mainError(), error); 113 | finalCallback(); 114 | }); 115 | }; 116 | 117 | fn(); 118 | })(); 119 | 120 | (function testRetryForever() { 121 | var error = new Error('some error'); 122 | var operation = retry.operation({ retries: 3, forever: true }); 123 | var attempts = 0; 124 | 125 | var finalCallback = fake.callback('finalCallback'); 126 | fake.expectAnytime(finalCallback); 127 | 128 | var fn = function() { 129 | operation.attempt(function(currentAttempt) { 130 | attempts++; 131 | assert.equal(currentAttempt, attempts); 132 | if (attempts !== 6 && operation.retry(error)) { 133 | return; 134 | } 135 | 136 | assert.strictEqual(attempts, 6); 137 | assert.strictEqual(operation.attempts(), attempts); 138 | assert.strictEqual(operation.mainError(), error); 139 | finalCallback(); 140 | }); 141 | }; 142 | 143 | fn(); 144 | })(); 145 | 146 | (function testRetryForeverNoRetries() { 147 | var error = new Error('some error'); 148 | var delay = 50 149 | var operation = retry.operation({ 150 | retries: null, 151 | forever: true, 152 | minTimeout: delay, 153 | maxTimeout: delay 154 | }); 155 | 156 | var attempts = 0; 157 | var startTime = new Date().getTime(); 158 | 159 | var finalCallback = fake.callback('finalCallback'); 160 | fake.expectAnytime(finalCallback); 161 | 162 | var fn = function() { 163 | operation.attempt(function(currentAttempt) { 164 | attempts++; 165 | assert.equal(currentAttempt, attempts); 166 | if (attempts !== 4 && operation.retry(error)) { 167 | return; 168 | } 169 | 170 | var endTime = new Date().getTime(); 171 | var minTime = startTime + (delay * 3); 172 | var maxTime = minTime + 20 // add a little headroom for code execution time 173 | assert(endTime >= minTime) 174 | assert(endTime < maxTime) 175 | assert.strictEqual(attempts, 4); 176 | assert.strictEqual(operation.attempts(), attempts); 177 | assert.strictEqual(operation.mainError(), error); 178 | finalCallback(); 179 | }); 180 | }; 181 | 182 | fn(); 183 | })(); 184 | 185 | (function testStop() { 186 | var error = new Error('some error'); 187 | var operation = retry.operation([1, 2, 3]); 188 | var attempts = 0; 189 | 190 | var finalCallback = fake.callback('finalCallback'); 191 | fake.expectAnytime(finalCallback); 192 | 193 | var fn = function() { 194 | operation.attempt(function(currentAttempt) { 195 | attempts++; 196 | assert.equal(currentAttempt, attempts); 197 | 198 | if (attempts === 2) { 199 | operation.stop(); 200 | 201 | assert.strictEqual(attempts, 2); 202 | assert.strictEqual(operation.attempts(), attempts); 203 | assert.strictEqual(operation.mainError(), error); 204 | finalCallback(); 205 | } 206 | 207 | if (operation.retry(error)) { 208 | return; 209 | } 210 | }); 211 | }; 212 | 213 | fn(); 214 | })(); 215 | 216 | (function testMaxRetryTime() { 217 | var error = new Error('some error'); 218 | var maxRetryTime = 30; 219 | var operation = retry.operation({ 220 | minTimeout: 1, 221 | maxRetryTime: maxRetryTime 222 | }); 223 | var attempts = 0; 224 | 225 | var finalCallback = fake.callback('finalCallback'); 226 | fake.expectAnytime(finalCallback); 227 | 228 | var longAsyncFunction = function (wait, callback){ 229 | setTimeout(callback, wait); 230 | }; 231 | 232 | var fn = function() { 233 | var startTime = new Date().getTime(); 234 | operation.attempt(function(currentAttempt) { 235 | attempts++; 236 | assert.equal(currentAttempt, attempts); 237 | 238 | if (attempts !== 2) { 239 | if (operation.retry(error)) { 240 | return; 241 | } 242 | } else { 243 | var curTime = new Date().getTime(); 244 | longAsyncFunction(maxRetryTime - (curTime - startTime - 1), function(){ 245 | if (operation.retry(error)) { 246 | assert.fail('timeout should be occurred'); 247 | return; 248 | } 249 | 250 | assert.strictEqual(operation.mainError(), error); 251 | finalCallback(); 252 | }); 253 | } 254 | }); 255 | }; 256 | 257 | fn(); 258 | })(); 259 | 260 | (function testErrorsPreservedWhenMaxRetryTimeExceeded() { 261 | var error = new Error('some error'); 262 | var maxRetryTime = 30; 263 | var operation = retry.operation({ 264 | minTimeout: 1, 265 | maxRetryTime: maxRetryTime 266 | }); 267 | 268 | var finalCallback = fake.callback('finalCallback'); 269 | fake.expectAnytime(finalCallback); 270 | 271 | var longAsyncFunction = function (wait, callback){ 272 | setTimeout(callback, wait); 273 | }; 274 | 275 | var fn = function() { 276 | var startTime = new Date().getTime(); 277 | operation.attempt(function() { 278 | 279 | var curTime = new Date().getTime(); 280 | longAsyncFunction(maxRetryTime - (curTime - startTime - 1), function(){ 281 | if (operation.retry(error)) { 282 | assert.fail('timeout should be occurred'); 283 | return; 284 | } 285 | 286 | assert.strictEqual(operation.mainError(), error); 287 | finalCallback(); 288 | }); 289 | }); 290 | }; 291 | 292 | fn(); 293 | })(); 294 | -------------------------------------------------------------------------------- /test/integration/test-retry-wrap.js: -------------------------------------------------------------------------------- 1 | var common = require('../common'); 2 | var assert = common.assert; 3 | var fake = common.fake.create(); 4 | var retry = require(common.dir.lib + '/retry'); 5 | 6 | function getLib() { 7 | return { 8 | fn1: function() {}, 9 | fn2: function() {}, 10 | fn3: function() {} 11 | }; 12 | } 13 | 14 | (function wrapAll() { 15 | var lib = getLib(); 16 | retry.wrap(lib); 17 | assert.equal(lib.fn1.name, 'bound retryWrapper'); 18 | assert.equal(lib.fn2.name, 'bound retryWrapper'); 19 | assert.equal(lib.fn3.name, 'bound retryWrapper'); 20 | }()); 21 | 22 | (function wrapAllPassOptions() { 23 | var lib = getLib(); 24 | retry.wrap(lib, {retries: 2}); 25 | assert.equal(lib.fn1.name, 'bound retryWrapper'); 26 | assert.equal(lib.fn2.name, 'bound retryWrapper'); 27 | assert.equal(lib.fn3.name, 'bound retryWrapper'); 28 | assert.equal(lib.fn1.options.retries, 2); 29 | assert.equal(lib.fn2.options.retries, 2); 30 | assert.equal(lib.fn3.options.retries, 2); 31 | }()); 32 | 33 | (function wrapDefined() { 34 | var lib = getLib(); 35 | retry.wrap(lib, ['fn2', 'fn3']); 36 | assert.notEqual(lib.fn1.name, 'bound retryWrapper'); 37 | assert.equal(lib.fn2.name, 'bound retryWrapper'); 38 | assert.equal(lib.fn3.name, 'bound retryWrapper'); 39 | }()); 40 | 41 | (function wrapDefinedAndPassOptions() { 42 | var lib = getLib(); 43 | retry.wrap(lib, {retries: 2}, ['fn2', 'fn3']); 44 | assert.notEqual(lib.fn1.name, 'bound retryWrapper'); 45 | assert.equal(lib.fn2.name, 'bound retryWrapper'); 46 | assert.equal(lib.fn3.name, 'bound retryWrapper'); 47 | assert.equal(lib.fn2.options.retries, 2); 48 | assert.equal(lib.fn3.options.retries, 2); 49 | }()); 50 | 51 | (function runWrappedWithoutError() { 52 | var callbackCalled; 53 | var lib = {method: function(a, b, callback) { 54 | assert.equal(a, 1); 55 | assert.equal(b, 2); 56 | assert.equal(typeof callback, 'function'); 57 | callback(); 58 | }}; 59 | retry.wrap(lib); 60 | lib.method(1, 2, function() { 61 | callbackCalled = true; 62 | }); 63 | assert.ok(callbackCalled); 64 | }()); 65 | 66 | (function runWrappedSeveralWithoutError() { 67 | var callbacksCalled = 0; 68 | var lib = { 69 | fn1: function (a, callback) { 70 | assert.equal(a, 1); 71 | assert.equal(typeof callback, 'function'); 72 | callback(); 73 | }, 74 | fn2: function (a, callback) { 75 | assert.equal(a, 2); 76 | assert.equal(typeof callback, 'function'); 77 | callback(); 78 | } 79 | }; 80 | retry.wrap(lib, {}, ['fn1', 'fn2']); 81 | lib.fn1(1, function() { 82 | callbacksCalled++; 83 | }); 84 | lib.fn2(2, function() { 85 | callbacksCalled++; 86 | }); 87 | assert.equal(callbacksCalled, 2); 88 | }()); 89 | 90 | (function runWrappedWithError() { 91 | var callbackCalled; 92 | var lib = {method: function(callback) { 93 | callback(new Error('Some error')); 94 | }}; 95 | retry.wrap(lib, {retries: 1}); 96 | lib.method(function(err) { 97 | callbackCalled = true; 98 | assert.ok(err instanceof Error); 99 | }); 100 | assert.ok(!callbackCalled); 101 | }()); 102 | -------------------------------------------------------------------------------- /test/integration/test-timeouts.js: -------------------------------------------------------------------------------- 1 | var common = require('../common'); 2 | var assert = common.assert; 3 | var retry = require(common.dir.lib + '/retry'); 4 | 5 | (function testDefaultValues() { 6 | var timeouts = retry.timeouts(); 7 | 8 | assert.equal(timeouts.length, 10); 9 | assert.equal(timeouts[0], 1000); 10 | assert.equal(timeouts[1], 2000); 11 | assert.equal(timeouts[2], 4000); 12 | })(); 13 | 14 | (function testDefaultValuesWithRandomize() { 15 | var minTimeout = 5000; 16 | var timeouts = retry.timeouts({ 17 | minTimeout: minTimeout, 18 | randomize: true 19 | }); 20 | 21 | assert.equal(timeouts.length, 10); 22 | assert.ok(timeouts[0] > minTimeout); 23 | assert.ok(timeouts[1] > timeouts[0]); 24 | assert.ok(timeouts[2] > timeouts[1]); 25 | })(); 26 | 27 | (function testPassedTimeoutsAreUsed() { 28 | var timeoutsArray = [1000, 2000, 3000]; 29 | var timeouts = retry.timeouts(timeoutsArray); 30 | assert.deepEqual(timeouts, timeoutsArray); 31 | assert.notStrictEqual(timeouts, timeoutsArray); 32 | })(); 33 | 34 | (function testTimeoutsAreWithinBoundaries() { 35 | var minTimeout = 1000; 36 | var maxTimeout = 10000; 37 | var timeouts = retry.timeouts({ 38 | minTimeout: minTimeout, 39 | maxTimeout: maxTimeout 40 | }); 41 | for (var i = 0; i < timeouts; i++) { 42 | assert.ok(timeouts[i] >= minTimeout); 43 | assert.ok(timeouts[i] <= maxTimeout); 44 | } 45 | })(); 46 | 47 | (function testTimeoutsAreIncremental() { 48 | var timeouts = retry.timeouts(); 49 | var lastTimeout = timeouts[0]; 50 | for (var i = 0; i < timeouts; i++) { 51 | assert.ok(timeouts[i] > lastTimeout); 52 | lastTimeout = timeouts[i]; 53 | } 54 | })(); 55 | 56 | (function testTimeoutsAreIncrementalForFactorsLessThanOne() { 57 | var timeouts = retry.timeouts({ 58 | retries: 3, 59 | factor: 0.5 60 | }); 61 | 62 | var expected = [250, 500, 1000]; 63 | assert.deepEqual(expected, timeouts); 64 | })(); 65 | 66 | (function testRetries() { 67 | var timeouts = retry.timeouts({retries: 2}); 68 | assert.strictEqual(timeouts.length, 2); 69 | })(); 70 | --------------------------------------------------------------------------------