├── .gitignore ├── .npmignore ├── History.md ├── Makefile ├── Readme.md ├── index.js ├── memory.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | support 2 | test 3 | examples 4 | *.sock 5 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 4.0.2 / 2016-07-17 3 | ================== 4 | 5 | * update the readme 6 | * fix tests for 4.x 7 | * add fatal function to crash and burn 8 | * add a memory check 9 | 10 | 4.0.1 / 2016-04-25 11 | ================== 12 | 13 | * pass along the arguments 14 | 15 | 4.0.0 / 2016-04-24 16 | ================== 17 | 18 | * modify defaults (retry = Infinity, timeout = 10s) 19 | 20 | 3.0.1 / 2016-04-01 21 | ================== 22 | 23 | * fix timeout errors in try-again 24 | 25 | 3.0.0 / 2016-03-24 26 | ================== 27 | 28 | * status now reports both successes and failures. added failed function when the failed attempts exceeds the retry threshold 29 | 30 | 2.0.0 / 2016-03-24 31 | ================== 32 | 33 | * updates signature to support successful connections. added tests. fixed some options 34 | * update readme 35 | * update the readme 36 | * use js formatting 37 | 38 | 1.0.4 / 2016-03-14 39 | ================== 40 | 41 | * export again 42 | 43 | 1.0.3 / 2016-03-14 44 | ================== 45 | 46 | * ensure that old successes arent called add timeout support 47 | 48 | 1.0.2 / 2016-03-14 49 | ================== 50 | 51 | * add default jitter and place unsuccessful into the instance 52 | * update the readme 53 | 54 | 1.0.1 / 2016-03-14 55 | ================== 56 | 57 | * ensure we only call these functions once 58 | 59 | 1.0.0 / 2010-01-03 60 | ================== 61 | 62 | * Initial release 63 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | @./node_modules/.bin/mocha \ 4 | --reporter spec 5 | 6 | .PHONY: test 7 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # try-again 3 | 4 | Generic, simple retry module with exponential backoff. 5 | 6 | ## Features 7 | 8 | - Easy to understand the different states 9 | - Safe functions by design (see below) 10 | - Exponential backoff 11 | - Supports timeouts 12 | - Fatal errors 13 | - Retries 14 | 15 | ## Installation 16 | 17 | ```js 18 | npm install try-again 19 | ``` 20 | 21 | ## Usage 22 | 23 | ```js 24 | var Again = require('try-again') 25 | var again = Again({ 26 | retries: 8, 27 | max: 10000, 28 | jitter: .2, 29 | factor: 2, 30 | min: 100 31 | }) 32 | 33 | // this function will get re-called each time there is 34 | // failure, unless retries is 0 or fatal(...) is called 35 | var client = again(function (success, failure, fatal) { 36 | var client = new Client(url) 37 | client.once('connected', success) 38 | client.once('close', failure) 39 | client.once('error', failure) 40 | return client 41 | }, status, failed) 42 | 43 | // this function will get called whenever one of the 44 | // 3 functions: success, failure, or fatal get called. 45 | // This function is often used to update connection state. 46 | function status (err) { 47 | if (err) { 48 | // there was a failure 49 | // update connection state accordingly 50 | } else { 51 | // there was a success 52 | // update connection state accordingly 53 | } 54 | } 55 | 56 | // this function is used when the retries have been 57 | // exhausted or the fatal function has been called. 58 | // at this point, there will be no more retries and 59 | // you should consider crashing the process. 60 | function failed (err) { 61 | console.error({ 62 | message: 'aborting, tried too many times' 63 | error: err.stack || err 64 | }) 65 | } 66 | ``` 67 | 68 | ## Design 69 | 70 | You don't need to know the details of how this works to use this module, but if you're interested in knowing how the different states may interact, this should help explain things. 71 | 72 | **Everything inside the `again` function should be idempotent** 73 | 74 | The function inside `again` will be called multiple times when there is a failure, so it's important that you don't have existing event emitters and other things hanging around. You should create a new client inside this function each time. 75 | 76 | **The `success` function only works once and only if `failure` has not already been called** 77 | 78 | `failure` may be called after `success` has been called, but `success` will be a noop if `failure` has been called. This is to prevent multiple `success` functions from running if the connection is eventually successful. 79 | 80 | **The `status` function will be called each time there is an update, either a successful connection or a failure** 81 | 82 | If there is a failure, the `err` parameter will be populated. This function may be called multiple times. It's a good place for logging connection status and setting "connected" state. 83 | 84 | **The `failed` function will only be called if the number of attempts to connect have exceeded the retries option** 85 | 86 | If the `failed` function is called, it won't try anymore. You probably want to handle this case by failing fast and killing the process. 87 | 88 | **The `fatal` function may be used to trigger the `failed` function even if there are retries available.** 89 | 90 | You can use the fatal function to skip retrying. Like the `failure` function, the `fatal` function may be called after an initial `success` function, but cannot be called after the `failure` function has previously been called for that cycle. 91 | 92 | ## Running Tests 93 | 94 | ``` 95 | npm install 96 | make test 97 | ``` 98 | 99 | ## License 100 | 101 | MIT 102 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var errors = require('combine-errors') 6 | var debug = require('debug')('again') 7 | var uniq = require('lodash.uniqby') 8 | var Backoff = require('backo') 9 | var sliced = require('sliced') 10 | var once = require('once') 11 | 12 | /** 13 | * Export `Again` 14 | */ 15 | 16 | module.exports = Again 17 | 18 | /** 19 | * Create an `Again` instance 20 | */ 21 | 22 | function Again (options) { 23 | options = options || {} 24 | 25 | options.timeout = options.timeout === undefined ? 10000 : options.timeout 26 | options.retries = options.retries === undefined ? Infinity : options.retries 27 | options.jitter = options.jitter === undefined ? 0.3 : options.jitter 28 | 29 | return function again (fn, status, failed) { 30 | status = status || function(){} 31 | failed = failed || function(){} 32 | 33 | // only let failed get called once 34 | failed = once(failed) 35 | 36 | var backo = new Backoff(options) 37 | var timeout = options.timeout 38 | var retries = options.retries 39 | var tid = null 40 | var errs = [] 41 | var sid = 0 42 | 43 | return retry() 44 | 45 | function retry () { 46 | var succeed = once(success(sid)) 47 | var exit = once(fatal(sid)) 48 | var fail = once(failure) 49 | 50 | tid = setTimeout(function() { 51 | debug('timed out after %sms', timeout) 52 | failure(new Error('operation timed out')) 53 | }, timeout) 54 | return fn(succeed, fail, exit) 55 | } 56 | 57 | function success (id) { 58 | return function () { 59 | if (sid !== id) return 60 | debug('success') 61 | tid && clearTimeout(tid) 62 | retries = options.retries 63 | backo.reset() 64 | // report a success 65 | status.apply(null, [null].concat(sliced(arguments))) 66 | } 67 | } 68 | 69 | function failure (err) { 70 | debug('failure') 71 | 72 | tid && clearTimeout(tid) 73 | err && errs.push(err) 74 | 75 | // don't let an old success id get called 76 | // after we've called failure. this does 77 | // not apply the other way, you can call 78 | // failure after you call success 79 | sid++ 80 | 81 | // report a failure 82 | status(err) 83 | 84 | if (--retries <= 0) { 85 | errs = uniq(errs, function (err) { 86 | return err.message 87 | }) 88 | return failed(errors(errs)) 89 | } 90 | 91 | var duration = backo.duration() 92 | debug('sleeping for %sms', duration) 93 | setTimeout(function() { 94 | debug('trying again') 95 | retry() 96 | }, duration) 97 | } 98 | 99 | // fatal is a condition where we don't 100 | // want to retry and we just want to 101 | // fail completely. we don't want to call 102 | // this after failure has already been 103 | // called, so we use the id. This can 104 | // be called after success has been 105 | // called though. 106 | function fatal (id) { 107 | return function (err) { 108 | if (sid !== id) return 109 | debug('fatal') 110 | 111 | // set the retries to 0 112 | // so we don't retry again 113 | retries = 0 114 | 115 | // call failure 116 | return failure(err) 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /memory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | let memwatch = require('memwatch-next') 6 | let fs = require('fs') 7 | let Again = require('./') 8 | 9 | memwatch.on('leak', function(info) { 10 | console.log('LEAK', info) 11 | }) 12 | 13 | // memwatch.on('stats', function(info) { 14 | // console.log('stats', info) 15 | // }) 16 | 17 | function FaultyClient (ms) { 18 | return function client (fn) { 19 | fs.readFile('./index.js', function(err, buf) { 20 | // console.log('buffer', buf) 21 | fn(new Error('unable to connect')) 22 | }) 23 | } 24 | } 25 | 26 | let again = Again()(function (success, failure) { 27 | let client = FaultyClient() 28 | client(function(err) { 29 | if (err) return failure(err) 30 | success() 31 | }) 32 | }, status, failed) 33 | 34 | 35 | function status(connected) { 36 | console.log('failed to connect') 37 | // console.log('connected?', connected) 38 | } 39 | 40 | function failed (error) { 41 | console.log('failed', error) 42 | } 43 | 44 | function generateHeapDumpAndStats(){ 45 | //1. Force garbage collection every time this function is called 46 | try { 47 | global.gc(); 48 | } catch (e) { 49 | console.log("You must run program with 'node --expose-gc index.js' or 'npm start'"); 50 | process.exit(); 51 | } 52 | 53 | // 2. Output Heap stats 54 | var heapUsed = process.memoryUsage().heapUsed; 55 | console.log("Program is using " + heapUsed + " bytes of Heap.") 56 | 57 | //3. Get Heap dump 58 | // process.kill(process.pid, 'SIGUSR2'); 59 | } 60 | 61 | setInterval(generateHeapDumpAndStats, 1000) 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "try-again", 3 | "version": "4.0.2", 4 | "description": "Generic retry module with exponential backoff", 5 | "keywords": [], 6 | "author": "Matthew Mueller ", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/MatthewMueller/try-again.git" 10 | }, 11 | "dependencies": { 12 | "backo": "1.1.0", 13 | "combine-errors": "3.0.3", 14 | "debug": "2.2.0", 15 | "lodash.uniqby": "4.5.0", 16 | "once": "1.3.3", 17 | "sliced": "1.0.1" 18 | }, 19 | "devDependencies": { 20 | "mocha": "2.5.3" 21 | }, 22 | "main": "index" 23 | } -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var Emitter = require('events').EventEmitter 6 | var assert = require('assert') 7 | var Again = require('./') 8 | 9 | /** 10 | * Tests 11 | */ 12 | 13 | describe('tryagain', function() { 14 | 15 | it('should support a successful connection', function(done) { 16 | var client = Client({ 17 | success: [100] 18 | }) 19 | 20 | var again = Again() 21 | again(function (success, failure) { 22 | var c = client() 23 | c.on('success', success) 24 | c.on('failure', failure) 25 | }, done) 26 | }) 27 | 28 | it('should support connecting after a bit', function(done) { 29 | var client = Client({ 30 | success: [1000] 31 | }) 32 | 33 | var again = Again() 34 | again(function (success, failure) { 35 | var c = client() 36 | c.on('success', success) 37 | c.on('failure', failure) 38 | }, done) 39 | }) 40 | 41 | it('should support the failure case', function(done) { 42 | var client = Client({ 43 | failure: [100, 100, 100, 100, 100, 100, 100, 100, 100] 44 | }) 45 | 46 | var again = Again({ max: 200, retries: 7 }) 47 | var called = 0 48 | again(function (success, failure) { 49 | var c = client() 50 | c.on('success', success) 51 | c.on('failure', failure) 52 | }, function status (err) { 53 | assert.equal(err.message, 'failure') 54 | called++ 55 | }, function failed (err) { 56 | assert.equal(err.message, 'failure') 57 | assert.equal(called, 7) 58 | done() 59 | }) 60 | }) 61 | 62 | it('should support failing after a bit', function(done) { 63 | var client = Client({ 64 | failure: [500, 500, 500] 65 | }) 66 | 67 | var again = Again({ retries: 2, max: 200 }) 68 | var called = 0 69 | 70 | again(function (success, failure) { 71 | var c = client() 72 | c.on('success', success) 73 | c.on('failure', failure) 74 | }, function(err) { 75 | assert.equal(err.message, 'failure') 76 | called++ 77 | }, function(err) { 78 | assert.equal(err.message, 'failure') 79 | assert.equal(called, 2) 80 | done() 81 | }) 82 | }) 83 | 84 | it('should support failing, then connecting', function(done) { 85 | var client = Client({ 86 | success: [200, 200], 87 | failure: [100] 88 | }) 89 | 90 | var again = Again() 91 | var called = 0 92 | again(function (success, failure) { 93 | var c = client() 94 | c.on('success', success) 95 | c.on('failure', failure) 96 | }, function(err) { 97 | if (err) { 98 | assert.equal(err.message, 'failure') 99 | called++ 100 | } else { 101 | assert.equal(called, 1) 102 | done() 103 | } 104 | }, done) 105 | }) 106 | 107 | it('should support connecting, then failing twice, then connecting', function(done) { 108 | var client = Client({ 109 | success: [100, null, 100], 110 | failure: [200, 100, null] 111 | }) 112 | 113 | var again = Again() 114 | var successes = 0 115 | var failures = 0 116 | again(function (success, failure) { 117 | var c = client() 118 | c.on('success', success) 119 | c.on('failure', failure) 120 | }, function(err) { 121 | if (err) { 122 | failures++ 123 | assert.equal(err.message, 'failure') 124 | } else { 125 | successes++ 126 | if (successes === 2) { 127 | assert.equal(failures, 2) 128 | done() 129 | } 130 | } 131 | }, done) 132 | }) 133 | 134 | it('should eventually connect after timeouts', function(done) { 135 | var client = Client({ 136 | success: [500, 300, 100] 137 | }) 138 | 139 | var again = Again({ timeout: 200, jitter: 0, min: 80 }) 140 | var connected = 3 141 | again(function (success, failure) { 142 | var c = client() 143 | c.on('success', success) 144 | c.on('failure', failure) 145 | }, function status(err) { 146 | if (--connected) { 147 | assert.equal(err.message, 'operation timed out') 148 | } else { 149 | assert.equal(err, null) 150 | done(err) 151 | } 152 | }, done) 153 | }) 154 | 155 | it('should allow you to fail completely and not retry again', function(done) { 156 | var client = Client({ 157 | success: [500, 300, 100] 158 | }) 159 | 160 | var again = Again({ timeout: 200, jitter: 0, min: 80 }) 161 | var retried = 3 162 | var called = 0 163 | again(function (success, failure, fatal) { 164 | var c = client() 165 | c.on('success', success) 166 | c.on('failure', failure) 167 | if (!--retried) { 168 | return fatal(new Error('abruptly exited')) 169 | } 170 | }, function status (err) { 171 | called++ 172 | if (retried && err) { 173 | assert.equal(err.message, 'operation timed out') 174 | } else { 175 | assert.equal(err.message, 'abruptly exited') 176 | } 177 | }, function (err) { 178 | assert.equal(called, 3) 179 | assert.equal(err.message, 'operation timed out; abruptly exited') 180 | done() 181 | }) 182 | }) 183 | }) 184 | 185 | /** 186 | * Create a client 187 | */ 188 | 189 | function Client (states, error) { 190 | states.success = states.success || [] 191 | states.failure = states.failure || [] 192 | var idx = -1 193 | 194 | return function client () { 195 | var emitter = new Emitter() 196 | idx++ 197 | 198 | if (states.success[idx]) { 199 | setTimeout(function() { 200 | emitter.emit('success') 201 | }, states.success[idx]) 202 | } 203 | 204 | if (states.failure[idx]) { 205 | setTimeout(function() { 206 | emitter.emit('failure', error || new Error('failure')) 207 | }, states.failure[idx]) 208 | } 209 | 210 | return emitter 211 | } 212 | } 213 | --------------------------------------------------------------------------------