├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples └── simple.js ├── index.js ├── package.json ├── reconnect.js └── test └── simple-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by http://gitignore.io 2 | 3 | ### Node ### 4 | lib-cov 5 | *.seed 6 | *.log 7 | *.csv 8 | *.dat 9 | *.out 10 | *.pid 11 | *.gz 12 | 13 | pids 14 | logs 15 | results 16 | 17 | npm-debug.log 18 | node_modules 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_install: 3 | - curl --location http://git.io/1OcIZA | bash -s 4 | node_js: 5 | - "4" 6 | - "6" 7 | - "8" 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2013 Jarrett Cruger 3 | 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 all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # back 2 | 3 | [![build 4 | status](https://secure.travis-ci.org/jcrugzz/back.png)](http://travis-ci.org/jcrugzz/back) 5 | 6 | [![NPM](https://nodei.co/npm/back.png)](https://nodei.co/npm/back/) 7 | 8 | A simple module to be used for creating exponentially weighted backoff attempts. 9 | Originally extracted from [Primus][Primus]. 10 | 11 | __NOTICE__ 12 | If you were a pre-1.0.0 `back` user, the API has changed to what is found below. 13 | If you do not like this slightly different abstraction and would prefer the 14 | former, slightly simpler API, it is still available with `require('back/reconnect')`. 15 | 16 | The API change thanks to a contribution from 17 | [@Raynos](https://github.com/Raynos) makes things simpler as you don't have to 18 | manage the copying of the options object yourself in order to handle repeated 19 | backoff cases. 20 | 21 | ## Example 22 | 23 | ```js 24 | var http = require('http'); 25 | var back = require('back'); 26 | // 27 | // Options to use for backoff 28 | // 29 | // Remark: This object is modified so it should be cloned if you are dealing 30 | // with independent backoff attempts and want to use these values as a base. 31 | // 32 | var options = { 33 | retries: 3, 34 | minDelay: 1000, // Defaults to 500ms 35 | maxDelay: 10000, // Defaults to infinity 36 | // The following option is shown with its default value but you will most 37 | // likely never define it as it creates the exponential curve. 38 | factor: 2, 39 | }; 40 | 41 | // Where we will store the backoff instance during a particular backoff attempt 42 | var attempt; 43 | 44 | function retry(err) { 45 | var back = attempt || (attempt = new Back(options)); 46 | return back.backoff(function (fail) { 47 | if (fail) { 48 | // Oh noez we never reconnect :( 49 | console.error('Retry failed with ' + err.message); 50 | process.exit(1); 51 | } 52 | // 53 | // Remark: .attempt and .timeout are added to this object internally 54 | // 55 | console.log('Retry attempt # ' + back.settings.attempt + 56 | ' being made after ' + back.settings.timeout + 'ms'); 57 | request(); 58 | }); 59 | } 60 | 61 | function request() { 62 | http.get('http://localhost:9000', function (res) { 63 | console.log('Successful Response that will not happen!'); 64 | // 65 | // If we succeeded, we would set the current to null so the next error 66 | // generates a new instance. 67 | // 68 | attempt = null; 69 | }).on('error', retry); 70 | } 71 | 72 | request(); 73 | ``` 74 | 75 | ## API 76 | 77 | ### `var back = new Back(backoffOpts);` 78 | 79 | The `Back` constructor function takes your backoff options and saves them as 80 | `settings` in the internal state of the `back` object. 81 | 82 | #### `back.backoff(callback)` 83 | 84 | The `back` instance has a `backoff` method that takes a `callback` that is 85 | executed after a `setTimeout`. The timeout is what is based on an [exponential 86 | backoff](http://dthain.blogspot.nl/2009/02/exponential-backoff-in-distributed.html) of course! 87 | It will repeatedly all this callback based on the backoff options you passed to 88 | the back instance until it exhausts its efforts. When it has exhausted its 89 | attempts, it will return an error as the first argument to the callback. 90 | 91 | #### `back.close()` 92 | 93 | Clear backoff timer in cases where you want to dispose of the instance before the `callback` is executed. 94 | 95 | [Primus]: https://github.com/3rd-Eden/primus 96 | 97 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var back = require('../'); 3 | // 4 | // Options to use for backoff 5 | // 6 | // Remark: This object is modified so it should be cloned if you are dealing 7 | // with independent backoff attempts and want to use these values as a base. 8 | // 9 | var backoff = { 10 | retries: 3, 11 | minDelay: 1000, // Defaults to 500ms 12 | maxDelay: 10000, // Defaults to infinity 13 | // The following option is shown with its default value but you will most 14 | // likely never define it as it creates the exponential curve. 15 | factor: 2, 16 | }; 17 | 18 | function retry(err) { 19 | return back(function (fail) { 20 | if (fail) { 21 | // Oh noez we never reconnect :( 22 | console.error('Retry failed with ' + err.message); 23 | process.exit(1); 24 | } 25 | // 26 | // Remark: .attempt and .timeout are added to this object internally 27 | // 28 | console.log('Retry attempt # ' + backoff.attempt + 29 | ' being made after ' + backoff.timeout + 'ms'); 30 | request(); 31 | }, backoff); 32 | } 33 | 34 | function request() { 35 | http.get('http://localhost:9000', function (res) { 36 | console.log('Successful Response that will not happen!'); 37 | }).on('error', retry); 38 | } 39 | 40 | request(); 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Reconnect = require('./reconnect'); 2 | var extend = require('xtend/immutable'); 3 | var clearTimeout = require('timers').clearTimeout; 4 | 5 | module.exports = Back; 6 | 7 | // 8 | // Takes a set of reconnect options defined in README 9 | // 10 | function Back(options) { 11 | if (!(this instanceof Back)) { 12 | return new Back(options); 13 | } 14 | 15 | this.settings = extend(options); 16 | this.reconnect = null; 17 | } 18 | 19 | Back.prototype.backoff = function backoff(cb) { 20 | this.reconnect = new Reconnect(cb, this.settings); 21 | }; 22 | 23 | Back.prototype.close = function close() { 24 | if (this.reconnect && this.reconnect.timer) { 25 | return clearTimeout(this.reconnect.timer); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "back", 3 | "description": "Simple exponential backoff pulled out of Primus by @3rd-Eden", 4 | "version": "1.0.2", 5 | "keywords": [ 6 | "random", 7 | "exponential", 8 | "backoff" 9 | ], 10 | "author": "Jarrett Cruger ", 11 | "contributors": [ 12 | "Jake Verbaten " 13 | ], 14 | "repository": { 15 | "url": "git://github.com/jcrugzz/back.git" 16 | }, 17 | "main": "index.js", 18 | "scripts": { 19 | "test": "tape test/*.js" 20 | }, 21 | "engines": { 22 | "node": ">=4.x || 0.12.x || 0.10.x || 0.8.x" 23 | }, 24 | "dependencies": { 25 | "xtend": "^4.0.0" 26 | }, 27 | "devDependencies": { 28 | "tape": "2.1.x" 29 | }, 30 | "license": "MIT" 31 | } 32 | -------------------------------------------------------------------------------- /reconnect.js: -------------------------------------------------------------------------------- 1 | 2 | var Reconnect = module.exports = function reconnect(callback, opts) { 3 | if (!(this instanceof Reconnect)) { 4 | return new Reconnect(callback, opts); 5 | } 6 | 7 | opts = opts || {}; 8 | 9 | if (opts.backoff) return; 10 | 11 | opts.maxDelay = opts.maxDelay || Infinity; // Maximum delay. 12 | opts.minDelay = opts.minDelay || 500; // Minimum delay. 13 | opts.retries = (opts.retries === 0 ? 0 : opts.retries) || 10; // Amount of allowed retries. 14 | opts.attempt = (+opts.attempt || 0) + 1; // Current attempt. 15 | opts.factor = opts.factor || 2; // Back off factor. 16 | 17 | // Bailout if we are about to make to much attempts. Please note that we use ... 18 | if (opts.attempt > opts.retries) { 19 | return callback(new Error('Unable to retry'), opts); 20 | } 21 | 22 | // Prevent duplicate back off attempts. 23 | opts.backoff = true; 24 | 25 | // 26 | // Calculate the timeout, but make it randomly so we don't retry connections 27 | // at the same interval and defeat the purpose. This exponential back off is 28 | // based on the work of: 29 | // 30 | // http://dthain.blogspot.nl/2009/02/exponential-backoff-in-distributed.html 31 | // 32 | opts.timeout = opts.attempt !== 1 33 | ? Math.min(Math.round( 34 | (Math.random() + 1) * opts.minDelay * Math.pow(opts.factor, opts.attempt) 35 | ), opts.maxDelay) 36 | : opts.minDelay; 37 | 38 | this.timer = setTimeout(function delay() { 39 | opts.backoff = false; 40 | clearTimeout(this.timer); 41 | 42 | callback(undefined, opts); 43 | }.bind(this), opts.timeout); 44 | 45 | }; 46 | -------------------------------------------------------------------------------- /test/simple-test.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var test = require('tape'); 3 | 4 | var Back = require('..'); 5 | 6 | test('wooo does exponential backoff work as expected?', function (t) { 7 | t.plan(3); 8 | var count = 0, 9 | timeouts = [1000, 4000, 9000]; 10 | // 11 | // Options to use for backoff 12 | // 13 | // Remark: This object is modified so it should be cloned if you are dealing 14 | // with independent backoff attempts and want to use these values as a base. 15 | // 16 | var options = { 17 | retries: 3, 18 | minDelay: 1000, // Defaults to 500ms 19 | maxDelay: 10000, // Defaults to infinity 20 | // The following option is shown with its default value 21 | // but you will most likely never define it. 22 | factor: 2, 23 | }; 24 | 25 | // Where we will store the backoff instance during a particular backoff attempt 26 | var attempt; 27 | 28 | function retry(err) { 29 | var back = attempt || (attempt = new Back(options)); 30 | return back.backoff(function (fail) { 31 | if (fail) { 32 | // Oh noez we never reconnect :( 33 | return t.end(); 34 | } 35 | 36 | t.ok(back.settings.timeout >= timeouts[count++], 'Successful backoff with timeout ' + 37 | back.settings.timeout); 38 | request(); 39 | }); 40 | } 41 | 42 | function request() { 43 | http.get('http://localhost:9000', function (res) { 44 | console.log('Successful Response that will not happen!'); 45 | // 46 | // If we succeeded, we would set the current to null so the next error 47 | // generates a new instance. 48 | attempt = null; 49 | }).on('error', retry); 50 | } 51 | 52 | request(); 53 | }); 54 | 55 | --------------------------------------------------------------------------------