├── .gitignore
├── .jshintrc
├── .travis.yml
├── CHANGES.md
├── LICENSE
├── README.md
├── docs
├── backoff.html
├── docco.css
├── exponential.html
├── fibonacci.html
├── function_call.html
├── img
│ ├── backoff_events.png
│ ├── function_call_events.png
│ └── layers.png
├── index.html
├── public
│ ├── fonts
│ │ ├── aller-bold.eot
│ │ ├── aller-bold.ttf
│ │ ├── aller-bold.woff
│ │ ├── aller-light.eot
│ │ ├── aller-light.ttf
│ │ ├── aller-light.woff
│ │ ├── novecento-bold.eot
│ │ ├── novecento-bold.ttf
│ │ └── novecento-bold.woff
│ └── stylesheets
│ │ └── normalize.css
└── strategy.html
├── examples
├── exponential.js
├── exponential_strategy.js
├── fail.js
├── fibonacci.js
├── fibonacci_strategy.js
├── function_call.js
├── randomized.js
├── readme.js
├── reset.js
└── set_timeout.js
├── index.js
├── lib
├── backoff.js
├── function_call.js
└── strategy
│ ├── exponential.js
│ ├── fibonacci.js
│ └── strategy.js
├── package.json
└── tests
├── api.js
├── backoff.js
├── backoff_strategy.js
├── exponential_backoff_strategy.js
├── fibonacci_backoff_strategy.js
└── function_call.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | // Based on jshint defaults: http://goo.gl/OpjUs
2 |
3 | {
4 | // If the scan should stop on first error.
5 | "passfail": false,
6 | // Maximum errors before stopping.
7 | "maxerr": 50,
8 |
9 |
10 | // Predefined globals
11 |
12 | // If the standard browser globals should be predefined.
13 | "browser": false,
14 | // If the Node.js environment globals should be predefined.
15 | "node": true,
16 | // If the Rhino environment globals should be predefined.
17 | "rhino": false,
18 | // If CouchDB globals should be predefined.
19 | "couch": false,
20 | // If the Windows Scripting Host environment globals should be predefined.
21 | "wsh": false,
22 |
23 | // If jQuery globals should be predefined.
24 | "jquery": false,
25 | // If Prototype and Scriptaculous globals should be predefined.
26 | "prototypejs": false,
27 | // If MooTools globals should be predefined.
28 | "mootools": false,
29 | // If Dojo Toolkit globals should be predefined.
30 | "dojo": false,
31 |
32 | // Custom predefined globals.
33 | "predef": [],
34 |
35 |
36 | // Development
37 |
38 | // If debugger statements should be allowed.
39 | "debug": false,
40 | // If logging globals should be predefined (console, alert, etc.).
41 | "devel": false,
42 |
43 |
44 | // ECMAScript 5
45 |
46 | // If ES5 syntax should be allowed.
47 | "es5": false,
48 | // Require the "use strict"; pragma.
49 | "strict": false,
50 | // If global "use strict"; should be allowed (also enables strict).
51 | "globalstrict": false,
52 |
53 |
54 | // The Good Parts
55 |
56 | // If automatic semicolon insertion should be tolerated.
57 | "asi": false,
58 | // If line breaks should not be checked, e.g. `return [\n] x`.
59 | "laxbreak": false,
60 | // If bitwise operators (&, |, ^, etc.) should not be allowed.
61 | "bitwise": false,
62 | // If assignments inside if, for and while should be allowed. Usually
63 | // conditions and loops are for comparison, not assignments.
64 | "boss": true,
65 | // If curly braces around all blocks should be required.
66 | "curly": true,
67 | // If === should be required.
68 | "eqeqeq": false,
69 | // If == null comparisons should be tolerated.
70 | "eqnull": false,
71 | // If eval should be allowed.
72 | "evil": true,
73 | // If ExpressionStatement should be allowed as Programs.
74 | "expr": false,
75 | // If `for in` loops must filter with `hasOwnPrototype`.
76 | "forin": false,
77 | // If immediate invocations must be wrapped in parens, e.g.
78 | // `( function(){}() );`.
79 | "immed": false,
80 | // If use before define should not be tolerated.
81 | "latedef": false,
82 | // If functions should be allowed to be defined within loops.
83 | "loopfunc": false,
84 | // If arguments.caller and arguments.callee should be disallowed.
85 | "noarg": false,
86 | // If the . should not be allowed in regexp literals.
87 | "regexp": false,
88 | // If unescaped first/last dash (-) inside brackets should be tolerated.
89 | "regexdash": false,
90 | // If script-targeted URLs should be tolerated.
91 | "scripturl": false,
92 | // If variable shadowing should be tolerated.
93 | "shadow": false,
94 | // If `new function () { ... };` and `new Object;` should be tolerated.
95 | "supernew": false,
96 | // If variables should be declared before used.
97 | "undef": true,
98 | // If `this` inside a non-constructor function is valid.
99 | "validthis": false,
100 | // If smarttabs should be tolerated
101 | // (http://www.emacswiki.org/emacs/SmartTabs).
102 | "smarttabs": false,
103 | // If the `__proto__` property should be allowed.
104 | "proto": false,
105 | // If one case switch statements should be allowed.
106 | "onecase": false,
107 | // If non-standard (but widely adopted) globals should be predefined.
108 | "nonstandard": false,
109 | // Allow multiline strings.
110 | "multistr": false,
111 | // If line breaks should not be checked around commas.
112 | "laxcomma": false,
113 | // If semicolons may be ommitted for the trailing statements inside of a
114 | // one-line blocks.
115 | "lastsemic": false,
116 | // If the `__iterator__` property should be allowed.
117 | "iterator": false,
118 | // If only function scope should be used for scope tests.
119 | "funcscope": false,
120 | // If es.next specific syntax should be allowed.
121 | "esnext": false,
122 |
123 |
124 | // Style preferences
125 |
126 | // If constructor names must be capitalized.
127 | "newcap": true,
128 | // If empty blocks should be disallowed.
129 | "noempty": false,
130 | // If using `new` for side-effects should be disallowed.
131 | "nonew": false,
132 | // If names should be checked for leading or trailing underscores
133 | // (object._attribute would be disallowed).
134 | "nomen": false,
135 | // If only one var statement per function should be allowed.
136 | "onevar": false,
137 | // If increment and decrement (`++` and `--`) should not be allowed.
138 | "plusplus": false,
139 | // If all forms of subscript notation are tolerated.
140 | "sub": true,
141 | // If trailing whitespace rules apply.
142 | "trailing": true,
143 | // If strict whitespace rules apply.
144 | "white": false,
145 | // Specify indentation.
146 | "indent": 4
147 | }
148 |
149 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | before_install:
4 | - npm install -g npm
5 | - npm install -g jshint
6 |
7 | node_js:
8 | - "node"
9 | - "iojs"
10 |
11 | notifications:
12 | email:
13 | - turcotte.mat@gmail.com
14 |
15 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 2.5.0
4 |
5 | Those changes are not released yet.
6 |
7 | - In the functional API, invoke the wrapped function callback on abort and emit
8 | an `abort` event. This makes it possible to detect when abort is called.
9 | - Add a method on the function API, `call.retryIf(predicate)`, which specifies
10 | a predicate used to determine whether a given error is retriable or not. The
11 | default behavior is unaffected, errors remain retriable by default.
12 |
13 | ## 2.4.1
14 |
15 | - Add support for specifying the factor to use in the `ExponentialStrategy`.
16 |
17 | ## 2.4.0
18 |
19 | - Replace `FunctionCall.getResults` by `FunctionCall.getLastResult` to avoid
20 | storing intermediary results forever as this may lead to memory exhaustion
21 | when used in conjunction with an infinite number of backoffs.
22 | - Add `FunctionCall.getNumRetries` which returns the number of times the
23 | wrapped function was retried.
24 |
25 | ## 2.3.0
26 |
27 | - Add four new methods to `FunctionCall` to query the state of the call.
28 | - isPending
29 | - isRunning
30 | - isCompleted
31 | - isAborted
32 |
33 | ## 2.2.0
34 |
35 | - To match `Backoff` default behavior, `FunctionCall` no longer sets a
36 | default failAfter of 5, i.e. the maximum number of backoffs is now
37 | unbounded by default.
38 |
39 | ## 2.1.0
40 |
41 | - `Backoff.backoff` now accepts an optional error argument that is re-emitted
42 | as the last argument of the `backoff` and `fail` events. This provides some
43 | context to the listeners as to why a given backoff operation was attempted.
44 | - The `backoff` event emitted by the `FunctionCall` class now contains, as its
45 | last argument, the error that caused the backoff operation to be attempted.
46 | This provides some context to the listeners as to why a given backoff
47 | operation was attempted.
48 |
49 | ## 2.0.0
50 |
51 | - `FunctionCall.call` renamed into `FunctionCall.start`.
52 | - `backoff.call` no longer invokes the wrapped function on `nextTick`. That
53 | way, the first attempt is not delayed until the end of the current event
54 | loop.
55 |
56 | ## 1.2.1
57 |
58 | - Make `FunctionCall.backoffFactory` a private member.
59 |
60 | ## 1.2.0
61 |
62 | - Add `backoff.call` and the associated `FunctionCall` class.
63 |
64 | ## 1.1.0
65 |
66 | - Add a `Backoff.failAfter`.
67 |
68 | ## 1.0.0
69 |
70 | - Rename `start` and `done` events `backoff` and `ready`.
71 | - Remove deprecated `backoff.fibonnaci`.
72 |
73 | ## 0.2.1
74 |
75 | - Create `backoff.fibonacci`.
76 | - Deprecate `backoff.fibonnaci`.
77 | - Expose fibonacci and exponential strategies.
78 |
79 | ## 0.2.0
80 |
81 | - Provide exponential and fibonacci backoffs.
82 |
83 | ## 0.1.0
84 |
85 | - Change `initialTimeout` and `maxTimeout` to `initialDelay` and `maxDelay`.
86 | - Use fibonnaci backoff.
87 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2012 Mathieu Turcotte
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Backoff for Node.js
2 | [](http://travis-ci.org/MathieuTurcotte/node-backoff)
3 | [](http://badge.fury.io/js/backoff)
4 |
5 | Fibonacci and exponential backoffs for Node.js.
6 |
7 | ## Installation
8 |
9 | ```
10 | npm install backoff
11 | ```
12 |
13 | ## Unit tests
14 |
15 | ```
16 | npm test
17 | ```
18 |
19 | ## Usage
20 |
21 | ### Object Oriented
22 |
23 | The usual way to instantiate a new `Backoff` object is to use one predefined
24 | factory method: `backoff.fibonacci([options])`, `backoff.exponential([options])`.
25 |
26 | `Backoff` inherits from `EventEmitter`. When a backoff starts, a `backoff`
27 | event is emitted and, when a backoff ends, a `ready` event is emitted.
28 | Handlers for these two events are called with the current backoff number and
29 | delay.
30 |
31 | ``` js
32 | var backoff = require('backoff');
33 |
34 | var fibonacciBackoff = backoff.fibonacci({
35 | randomisationFactor: 0,
36 | initialDelay: 10,
37 | maxDelay: 300
38 | });
39 |
40 | fibonacciBackoff.failAfter(10);
41 |
42 | fibonacciBackoff.on('backoff', function(number, delay) {
43 | // Do something when backoff starts, e.g. show to the
44 | // user the delay before next reconnection attempt.
45 | console.log(number + ' ' + delay + 'ms');
46 | });
47 |
48 | fibonacciBackoff.on('ready', function(number, delay) {
49 | // Do something when backoff ends, e.g. retry a failed
50 | // operation (DNS lookup, API call, etc.). If it fails
51 | // again then backoff, otherwise reset the backoff
52 | // instance.
53 | fibonacciBackoff.backoff();
54 | });
55 |
56 | fibonacciBackoff.on('fail', function() {
57 | // Do something when the maximum number of backoffs is
58 | // reached, e.g. ask the user to check its connection.
59 | console.log('fail');
60 | });
61 |
62 | fibonacciBackoff.backoff();
63 | ```
64 |
65 | The previous example would print the following.
66 |
67 | ```
68 | 0 10ms
69 | 1 10ms
70 | 2 20ms
71 | 3 30ms
72 | 4 50ms
73 | 5 80ms
74 | 6 130ms
75 | 7 210ms
76 | 8 300ms
77 | 9 300ms
78 | fail
79 | ```
80 |
81 | Note that `Backoff` objects are meant to be instantiated once and reused
82 | several times by calling `reset` after a successful "retry".
83 |
84 | ### Functional
85 |
86 | It's also possible to avoid some boilerplate code when invoking an asynchronous
87 | function in a backoff loop by using `backoff.call(fn, [args, ...], callback)`.
88 |
89 | Typical usage looks like the following.
90 |
91 | ``` js
92 | var call = backoff.call(get, 'https://duplika.ca/', function(err, res) {
93 | console.log('Num retries: ' + call.getNumRetries());
94 |
95 | if (err) {
96 | console.log('Error: ' + err.message);
97 | } else {
98 | console.log('Status: ' + res.statusCode);
99 | }
100 | });
101 |
102 | call.retryIf(function(err) { return err.status == 503; });
103 | call.setStrategy(new backoff.ExponentialStrategy());
104 | call.failAfter(10);
105 | call.start();
106 | ```
107 |
108 | ## API
109 |
110 | ### backoff.fibonacci([options])
111 |
112 | Constructs a Fibonacci backoff (10, 10, 20, 30, 50, etc.).
113 |
114 | The options are the following.
115 |
116 | - randomisationFactor: defaults to 0, must be between 0 and 1
117 | - initialDelay: defaults to 100 ms
118 | - maxDelay: defaults to 10000 ms
119 |
120 | With these values, the backoff delay will increase from 100 ms to 10000 ms. The
121 | randomisation factor controls the range of randomness and must be between 0
122 | and 1. By default, no randomisation is applied on the backoff delay.
123 |
124 | ### backoff.exponential([options])
125 |
126 | Constructs an exponential backoff (10, 20, 40, 80, etc.).
127 |
128 | The options are the following.
129 |
130 | - randomisationFactor: defaults to 0, must be between 0 and 1
131 | - initialDelay: defaults to 100 ms
132 | - maxDelay: defaults to 10000 ms
133 | - factor: defaults to 2, must be greater than 1
134 |
135 | With these values, the backoff delay will increase from 100 ms to 10000 ms. The
136 | randomisation factor controls the range of randomness and must be between 0
137 | and 1. By default, no randomisation is applied on the backoff delay.
138 |
139 | ### backoff.call(fn, [args, ...], callback)
140 |
141 | - fn: function to call in a backoff handler, i.e. the wrapped function
142 | - args: function's arguments
143 | - callback: function's callback accepting an error as its first argument
144 |
145 | Constructs a `FunctionCall` instance for the given function. The wrapped
146 | function will get retried until it succeds or reaches the maximum number
147 | of backoffs. In both cases, the callback function will be invoked with the
148 | last result returned by the wrapped function.
149 |
150 | It is the caller's responsability to initiate the call by invoking the
151 | `start` method on the returned `FunctionCall` instance.
152 |
153 | ### Class Backoff
154 |
155 | #### new Backoff(strategy)
156 |
157 | - strategy: the backoff strategy to use
158 |
159 | Constructs a new backoff object from a specific backoff strategy. The backoff
160 | strategy must implement the `BackoffStrategy`interface defined bellow.
161 |
162 | #### backoff.failAfter(numberOfBackoffs)
163 |
164 | - numberOfBackoffs: maximum number of backoffs before the fail event gets
165 | emitted, must be greater than 0
166 |
167 | Sets a limit on the maximum number of backoffs that can be performed before
168 | a fail event gets emitted and the backoff instance is reset. By default, there
169 | is no limit on the number of backoffs that can be performed.
170 |
171 | #### backoff.backoff([err])
172 |
173 | Starts a backoff operation. If provided, the error parameter will be emitted
174 | as the last argument of the `backoff` and `fail` events to let the listeners
175 | know why the backoff operation was attempted.
176 |
177 | An error will be thrown if a backoff operation is already in progress.
178 |
179 | In practice, this method should be called after a failed attempt to perform a
180 | sensitive operation (connecting to a database, downloading a resource over the
181 | network, etc.).
182 |
183 | #### backoff.reset()
184 |
185 | Resets the backoff delay to the initial backoff delay and stop any backoff
186 | operation in progress. After reset, a backoff instance can and should be
187 | reused.
188 |
189 | In practice, this method should be called after having successfully completed
190 | the sensitive operation guarded by the backoff instance or if the client code
191 | request to stop any reconnection attempt.
192 |
193 | #### Event: 'backoff'
194 |
195 | - number: number of backoffs since last reset, starting at 0
196 | - delay: backoff delay in milliseconds
197 | - err: optional error parameter passed to `backoff.backoff([err])`
198 |
199 | Emitted when a backoff operation is started. Signals to the client how long
200 | the next backoff delay will be.
201 |
202 | #### Event: 'ready'
203 |
204 | - number: number of backoffs since last reset, starting at 0
205 | - delay: backoff delay in milliseconds
206 |
207 | Emitted when a backoff operation is done. Signals that the failing operation
208 | should be retried.
209 |
210 | #### Event: 'fail'
211 |
212 | - err: optional error parameter passed to `backoff.backoff([err])`
213 |
214 | Emitted when the maximum number of backoffs is reached. This event will only
215 | be emitted if the client has set a limit on the number of backoffs by calling
216 | `backoff.failAfter(numberOfBackoffs)`. The backoff instance is automatically
217 | reset after this event is emitted.
218 |
219 | ### Interface BackoffStrategy
220 |
221 | A backoff strategy must provide the following methods.
222 |
223 | #### strategy.next()
224 |
225 | Computes and returns the next backoff delay.
226 |
227 | #### strategy.reset()
228 |
229 | Resets the backoff delay to its initial value.
230 |
231 | ### Class ExponentialStrategy
232 |
233 | Exponential (10, 20, 40, 80, etc.) backoff strategy implementation.
234 |
235 | #### new ExponentialStrategy([options])
236 |
237 | The options are the following.
238 |
239 | - randomisationFactor: defaults to 0, must be between 0 and 1
240 | - initialDelay: defaults to 100 ms
241 | - maxDelay: defaults to 10000 ms
242 | - factor: defaults to 2, must be greater than 1
243 |
244 | ### Class FibonacciStrategy
245 |
246 | Fibonacci (10, 10, 20, 30, 50, etc.) backoff strategy implementation.
247 |
248 | #### new FibonacciStrategy([options])
249 |
250 | The options are the following.
251 |
252 | - randomisationFactor: defaults to 0, must be between 0 and 1
253 | - initialDelay: defaults to 100 ms
254 | - maxDelay: defaults to 10000 ms
255 |
256 | ### Class FunctionCall
257 |
258 | This class manages the calling of an asynchronous function within a backoff
259 | loop.
260 |
261 | This class should rarely be instantiated directly since the factory method
262 | `backoff.call(fn, [args, ...], callback)` offers a more convenient and safer
263 | way to create `FunctionCall` instances.
264 |
265 | #### new FunctionCall(fn, args, callback)
266 |
267 | - fn: asynchronous function to call
268 | - args: an array containing fn's args
269 | - callback: fn's callback
270 |
271 | Constructs a function handler for the given asynchronous function.
272 |
273 | #### call.isPending()
274 |
275 | Returns whether the call is pending, i.e. hasn't been started.
276 |
277 | #### call.isRunning()
278 |
279 | Returns whether the call is in progress.
280 |
281 | #### call.isCompleted()
282 |
283 | Returns whether the call is completed.
284 |
285 | #### call.isAborted()
286 |
287 | Returns whether the call is aborted.
288 |
289 | #### call.setStrategy(strategy)
290 |
291 | - strategy: strategy instance to use, defaults to `FibonacciStrategy`.
292 |
293 | Sets the backoff strategy to use. This method should be called before
294 | `call.start()` otherwise an exception will be thrown.
295 |
296 | #### call.failAfter(maxNumberOfBackoffs)
297 |
298 | - maxNumberOfBackoffs: maximum number of backoffs before the call is aborted
299 |
300 | Sets the maximum number of backoffs before the call is aborted. By default,
301 | there is no limit on the number of backoffs that can be performed.
302 |
303 | This method should be called before `call.start()` otherwise an exception will
304 | be thrown..
305 |
306 | #### call.retryIf(predicate)
307 |
308 | - predicate: a function which takes in as its argument the error returned
309 | by the wrapped function and determines whether it is retriable.
310 |
311 | Sets the predicate which will be invoked to determine whether a given error
312 | should be retried or not, e.g. a network error would be retriable while a type
313 | error would stop the function call. By default, all errors are considered to be
314 | retriable.
315 |
316 | This method should be called before `call.start()` otherwise an exception will
317 | be thrown.
318 |
319 | #### call.getLastResult()
320 |
321 | Returns an array containing the last arguments passed to the completion callback
322 | of the wrapped function. For example, to get the error code returned by the last
323 | call, one would do the following.
324 |
325 | ``` js
326 | var results = call.getLastResult();
327 | // The error code is the first parameter of the callback.
328 | var error = results[0];
329 | ```
330 |
331 | Note that if the call was aborted, it will contain the abort error and not the
332 | last error returned by the wrapped function.
333 |
334 | #### call.getNumRetries()
335 |
336 | Returns the number of times the wrapped function call was retried. For a
337 | wrapped function that succeeded immediately, this would return 0. This
338 | method can be called at any point in time during the call life cycle, i.e.
339 | before, during and after the wrapped function invocation.
340 |
341 | #### call.start()
342 |
343 | Initiates the call the wrapped function. This method should only be called
344 | once otherwise an exception will be thrown.
345 |
346 | #### call.abort()
347 |
348 | Aborts the call and causes the completion callback to be invoked with an abort
349 | error if the call was pending or running; does nothing otherwise. This method
350 | can safely be called mutliple times.
351 |
352 | #### Event: 'call'
353 |
354 | - args: wrapped function's arguments
355 |
356 | Emitted each time the wrapped function is called.
357 |
358 | #### Event: 'callback'
359 |
360 | - results: wrapped function's return values
361 |
362 | Emitted each time the wrapped function invokes its callback.
363 |
364 | #### Event: 'backoff'
365 |
366 | - number: backoff number, starts at 0
367 | - delay: backoff delay in milliseconds
368 | - err: the error that triggered the backoff operation
369 |
370 | Emitted each time a backoff operation is started.
371 |
372 | #### Event: 'abort'
373 |
374 | Emitted when a call is aborted.
375 |
376 | ## Annotated source code
377 |
378 | The annotated source code can be found at [mathieuturcotte.github.io/node-backoff/docs](http://mathieuturcotte.github.io/node-backoff/docs/).
379 |
380 | ## License
381 |
382 | This code is free to use under the terms of the [MIT license](http://mturcotte.mit-license.org/).
383 |
--------------------------------------------------------------------------------
/docs/backoff.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | backoff.js
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Jump To …
17 | +
18 |
19 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
backoff.js
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
71 |
Copyright (c) 2012 Mathieu Turcotte
72 | Licensed under the MIT license.
73 |
74 |
75 |
76 |
77 | var events = require ('events' );
78 | var precond = require ('precond' );
79 | var util = require ('util' );
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
90 |
A class to hold the state of a backoff operation. Accepts a backoff strategy
91 | to generate the backoff delays.
92 |
93 |
94 |
95 | function Backoff (backoffStrategy) {
96 | events.EventEmitter.call(this );
97 |
98 | this .backoffStrategy_ = backoffStrategy;
99 | this .maxNumberOfRetry_ = -1 ;
100 | this .backoffNumber_ = 0 ;
101 | this .backoffDelay_ = 0 ;
102 | this .timeoutID_ = -1 ;
103 |
104 | this .handlers = {
105 | backoff: this .onBackoff_.bind(this )
106 | };
107 | }
108 | util.inherits(Backoff, events.EventEmitter);
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
119 |
Sets a limit, greater than 0, on the maximum number of backoffs. A ‘fail’
120 | event will be emitted when the limit is reached.
121 |
122 |
123 |
124 | Backoff.prototype.failAfter = function (maxNumberOfRetry) {
125 | precond.checkArgument(maxNumberOfRetry > 0 ,
126 | 'Expected a maximum number of retry greater than 0 but got %s.' ,
127 | maxNumberOfRetry);
128 |
129 | this .maxNumberOfRetry_ = maxNumberOfRetry;
130 | };
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
141 |
Starts a backoff operation. Accepts an optional parameter to let the
142 | listeners know why the backoff operation was started.
143 |
144 |
145 |
146 | Backoff.prototype.backoff = function (err) {
147 | precond.checkState(this .timeoutID_ === -1 , 'Backoff in progress.' );
148 |
149 | if (this .backoffNumber_ === this .maxNumberOfRetry_) {
150 | this .emit('fail' , err);
151 | this .reset();
152 | } else {
153 | this .backoffDelay_ = this .backoffStrategy_.next();
154 | this .timeoutID_ = setTimeout(this .handlers.backoff, this .backoffDelay_);
155 | this .emit('backoff' , this .backoffNumber_, this .backoffDelay_, err);
156 | }
157 | };
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
168 |
Handles the backoff timeout completion.
169 |
170 |
171 |
172 | Backoff.prototype.onBackoff_ = function () {
173 | this .timeoutID_ = -1 ;
174 | this .emit('ready' , this .backoffNumber_, this .backoffDelay_);
175 | this .backoffNumber_++;
176 | };
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
187 |
Stops any backoff operation and resets the backoff delay to its inital value.
188 |
189 |
190 |
191 | Backoff.prototype.reset = function () {
192 | this .backoffNumber_ = 0 ;
193 | this .backoffStrategy_.reset();
194 | clearTimeout(this .timeoutID_);
195 | this .timeoutID_ = -1 ;
196 | };
197 |
198 | module.exports = Backoff;
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
--------------------------------------------------------------------------------
/docs/docco.css:
--------------------------------------------------------------------------------
1 | /*--------------------- Typography ----------------------------*/
2 |
3 | @font-face {
4 | font-family: 'aller-light';
5 | src: url('public/fonts/aller-light.eot');
6 | src: url('public/fonts/aller-light.eot?#iefix') format('embedded-opentype'),
7 | url('public/fonts/aller-light.woff') format('woff'),
8 | url('public/fonts/aller-light.ttf') format('truetype');
9 | font-weight: normal;
10 | font-style: normal;
11 | }
12 |
13 | @font-face {
14 | font-family: 'aller-bold';
15 | src: url('public/fonts/aller-bold.eot');
16 | src: url('public/fonts/aller-bold.eot?#iefix') format('embedded-opentype'),
17 | url('public/fonts/aller-bold.woff') format('woff'),
18 | url('public/fonts/aller-bold.ttf') format('truetype');
19 | font-weight: normal;
20 | font-style: normal;
21 | }
22 |
23 | @font-face {
24 | font-family: 'novecento-bold';
25 | src: url('public/fonts/novecento-bold.eot');
26 | src: url('public/fonts/novecento-bold.eot?#iefix') format('embedded-opentype'),
27 | url('public/fonts/novecento-bold.woff') format('woff'),
28 | url('public/fonts/novecento-bold.ttf') format('truetype');
29 | font-weight: normal;
30 | font-style: normal;
31 | }
32 |
33 | /*--------------------- Layout ----------------------------*/
34 | html { height: 100%; }
35 | body {
36 | font-family: "aller-light";
37 | font-size: 14px;
38 | line-height: 18px;
39 | color: #30404f;
40 | margin: 0; padding: 0;
41 | height:100%;
42 | }
43 | #container { min-height: 100%; }
44 |
45 | a {
46 | color: #000;
47 | }
48 |
49 | b, strong {
50 | font-weight: normal;
51 | font-family: "aller-bold";
52 | }
53 |
54 | p {
55 | margin: 15px 0 0px;
56 | }
57 | .annotation ul, .annotation ol {
58 | margin: 25px 0;
59 | }
60 | .annotation ul li, .annotation ol li {
61 | font-size: 14px;
62 | line-height: 18px;
63 | margin: 10px 0;
64 | }
65 |
66 | h1, h2, h3, h4, h5, h6 {
67 | color: #112233;
68 | line-height: 1em;
69 | font-weight: normal;
70 | font-family: "novecento-bold";
71 | text-transform: uppercase;
72 | margin: 30px 0 15px 0;
73 | }
74 |
75 | h1 {
76 | margin-top: 40px;
77 | }
78 |
79 | hr {
80 | border: 0;
81 | background: 1px #ddd;
82 | height: 1px;
83 | margin: 20px 0;
84 | }
85 |
86 | pre, tt, code {
87 | font-size: 12px; line-height: 16px;
88 | font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace;
89 | margin: 0; padding: 0;
90 | }
91 | .annotation pre {
92 | display: block;
93 | margin: 0;
94 | padding: 7px 10px;
95 | background: #fcfcfc;
96 | -moz-box-shadow: inset 0 0 10px rgba(0,0,0,0.1);
97 | -webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.1);
98 | box-shadow: inset 0 0 10px rgba(0,0,0,0.1);
99 | overflow-x: auto;
100 | }
101 | .annotation pre code {
102 | border: 0;
103 | padding: 0;
104 | background: transparent;
105 | }
106 |
107 |
108 | blockquote {
109 | border-left: 5px solid #ccc;
110 | margin: 0;
111 | padding: 1px 0 1px 1em;
112 | }
113 | .sections blockquote p {
114 | font-family: Menlo, Consolas, Monaco, monospace;
115 | font-size: 12px; line-height: 16px;
116 | color: #999;
117 | margin: 10px 0 0;
118 | white-space: pre-wrap;
119 | }
120 |
121 | ul.sections {
122 | list-style: none;
123 | padding:0 0 5px 0;;
124 | margin:0;
125 | }
126 |
127 | /*
128 | Force border-box so that % widths fit the parent
129 | container without overlap because of margin/padding.
130 |
131 | More Info : http://www.quirksmode.org/css/box.html
132 | */
133 | ul.sections > li > div {
134 | -moz-box-sizing: border-box; /* firefox */
135 | -ms-box-sizing: border-box; /* ie */
136 | -webkit-box-sizing: border-box; /* webkit */
137 | -khtml-box-sizing: border-box; /* konqueror */
138 | box-sizing: border-box; /* css3 */
139 | }
140 |
141 |
142 | /*---------------------- Jump Page -----------------------------*/
143 | #jump_to, #jump_page {
144 | margin: 0;
145 | background: white;
146 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777;
147 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px;
148 | font: 16px Arial;
149 | cursor: pointer;
150 | text-align: right;
151 | list-style: none;
152 | }
153 |
154 | #jump_to a {
155 | text-decoration: none;
156 | }
157 |
158 | #jump_to a.large {
159 | display: none;
160 | }
161 | #jump_to a.small {
162 | font-size: 22px;
163 | font-weight: bold;
164 | color: #676767;
165 | }
166 |
167 | #jump_to, #jump_wrapper {
168 | position: fixed;
169 | right: 0; top: 0;
170 | padding: 10px 15px;
171 | margin:0;
172 | }
173 |
174 | #jump_wrapper {
175 | display: none;
176 | padding:0;
177 | }
178 |
179 | #jump_to:hover #jump_wrapper {
180 | display: block;
181 | }
182 |
183 | #jump_page {
184 | padding: 5px 0 3px;
185 | margin: 0 0 25px 25px;
186 | }
187 |
188 | #jump_page .source {
189 | display: block;
190 | padding: 15px;
191 | text-decoration: none;
192 | border-top: 1px solid #eee;
193 | }
194 |
195 | #jump_page .source:hover {
196 | background: #f5f5ff;
197 | }
198 |
199 | #jump_page .source:first-child {
200 | }
201 |
202 | /*---------------------- Low resolutions (> 320px) ---------------------*/
203 | @media only screen and (min-width: 320px) {
204 | .pilwrap { display: none; }
205 |
206 | ul.sections > li > div {
207 | display: block;
208 | padding:5px 10px 0 10px;
209 | }
210 |
211 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol {
212 | padding-left: 30px;
213 | }
214 |
215 | ul.sections > li > div.content {
216 | overflow-x:auto;
217 | -webkit-box-shadow: inset 0 0 5px #e5e5ee;
218 | box-shadow: inset 0 0 5px #e5e5ee;
219 | border: 1px solid #dedede;
220 | margin:5px 10px 5px 10px;
221 | padding-bottom: 5px;
222 | }
223 |
224 | ul.sections > li > div.annotation pre {
225 | margin: 7px 0 7px;
226 | padding-left: 15px;
227 | }
228 |
229 | ul.sections > li > div.annotation p tt, .annotation code {
230 | background: #f8f8ff;
231 | border: 1px solid #dedede;
232 | font-size: 12px;
233 | padding: 0 0.2em;
234 | }
235 | }
236 |
237 | /*---------------------- (> 481px) ---------------------*/
238 | @media only screen and (min-width: 481px) {
239 | #container {
240 | position: relative;
241 | }
242 | body {
243 | background-color: #F5F5FF;
244 | font-size: 15px;
245 | line-height: 21px;
246 | }
247 | pre, tt, code {
248 | line-height: 18px;
249 | }
250 | p, ul, ol {
251 | margin: 0 0 15px;
252 | }
253 |
254 |
255 | #jump_to {
256 | padding: 5px 10px;
257 | }
258 | #jump_wrapper {
259 | padding: 0;
260 | }
261 | #jump_to, #jump_page {
262 | font: 10px Arial;
263 | text-transform: uppercase;
264 | }
265 | #jump_page .source {
266 | padding: 5px 10px;
267 | }
268 | #jump_to a.large {
269 | display: inline-block;
270 | }
271 | #jump_to a.small {
272 | display: none;
273 | }
274 |
275 |
276 |
277 | #background {
278 | position: absolute;
279 | top: 0; bottom: 0;
280 | width: 350px;
281 | background: #fff;
282 | border-right: 1px solid #e5e5ee;
283 | z-index: -1;
284 | }
285 |
286 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol {
287 | padding-left: 40px;
288 | }
289 |
290 | ul.sections > li {
291 | white-space: nowrap;
292 | }
293 |
294 | ul.sections > li > div {
295 | display: inline-block;
296 | }
297 |
298 | ul.sections > li > div.annotation {
299 | max-width: 350px;
300 | min-width: 350px;
301 | min-height: 5px;
302 | padding: 13px;
303 | overflow-x: hidden;
304 | white-space: normal;
305 | vertical-align: top;
306 | text-align: left;
307 | }
308 | ul.sections > li > div.annotation pre {
309 | margin: 15px 0 15px;
310 | padding-left: 15px;
311 | }
312 |
313 | ul.sections > li > div.content {
314 | padding: 13px;
315 | vertical-align: top;
316 | border: none;
317 | -webkit-box-shadow: none;
318 | box-shadow: none;
319 | }
320 |
321 | .pilwrap {
322 | position: relative;
323 | display: inline;
324 | }
325 |
326 | .pilcrow {
327 | font: 12px Arial;
328 | text-decoration: none;
329 | color: #454545;
330 | position: absolute;
331 | top: 3px; left: -20px;
332 | padding: 1px 2px;
333 | opacity: 0;
334 | -webkit-transition: opacity 0.2s linear;
335 | }
336 | .for-h1 .pilcrow {
337 | top: 47px;
338 | }
339 | .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow {
340 | top: 35px;
341 | }
342 |
343 | ul.sections > li > div.annotation:hover .pilcrow {
344 | opacity: 1;
345 | }
346 | }
347 |
348 | /*---------------------- (> 1025px) ---------------------*/
349 | @media only screen and (min-width: 1025px) {
350 |
351 | body {
352 | font-size: 16px;
353 | line-height: 24px;
354 | }
355 |
356 | #background {
357 | width: 525px;
358 | }
359 | ul.sections > li > div.annotation {
360 | max-width: 525px;
361 | min-width: 525px;
362 | padding: 10px 25px 1px 50px;
363 | }
364 | ul.sections > li > div.content {
365 | padding: 9px 15px 16px 25px;
366 | }
367 | }
368 |
369 | /*---------------------- Syntax Highlighting -----------------------------*/
370 |
371 | td.linenos { background-color: #f0f0f0; padding-right: 10px; }
372 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; }
373 | /*
374 |
375 | github.com style (c) Vasily Polovnyov
376 |
377 | */
378 |
379 | pre code {
380 | display: block; padding: 0.5em;
381 | color: #000;
382 | background: #f8f8ff
383 | }
384 |
385 | pre .hljs-comment,
386 | pre .hljs-template_comment,
387 | pre .hljs-diff .hljs-header,
388 | pre .hljs-javadoc {
389 | color: #408080;
390 | font-style: italic
391 | }
392 |
393 | pre .hljs-keyword,
394 | pre .hljs-assignment,
395 | pre .hljs-literal,
396 | pre .hljs-css .hljs-rule .hljs-keyword,
397 | pre .hljs-winutils,
398 | pre .hljs-javascript .hljs-title,
399 | pre .hljs-lisp .hljs-title,
400 | pre .hljs-subst {
401 | color: #954121;
402 | /*font-weight: bold*/
403 | }
404 |
405 | pre .hljs-number,
406 | pre .hljs-hexcolor {
407 | color: #40a070
408 | }
409 |
410 | pre .hljs-string,
411 | pre .hljs-tag .hljs-value,
412 | pre .hljs-phpdoc,
413 | pre .hljs-tex .hljs-formula {
414 | color: #219161;
415 | }
416 |
417 | pre .hljs-title,
418 | pre .hljs-id {
419 | color: #19469D;
420 | }
421 | pre .hljs-params {
422 | color: #00F;
423 | }
424 |
425 | pre .hljs-javascript .hljs-title,
426 | pre .hljs-lisp .hljs-title,
427 | pre .hljs-subst {
428 | font-weight: normal
429 | }
430 |
431 | pre .hljs-class .hljs-title,
432 | pre .hljs-haskell .hljs-label,
433 | pre .hljs-tex .hljs-command {
434 | color: #458;
435 | font-weight: bold
436 | }
437 |
438 | pre .hljs-tag,
439 | pre .hljs-tag .hljs-title,
440 | pre .hljs-rules .hljs-property,
441 | pre .hljs-django .hljs-tag .hljs-keyword {
442 | color: #000080;
443 | font-weight: normal
444 | }
445 |
446 | pre .hljs-attribute,
447 | pre .hljs-variable,
448 | pre .hljs-instancevar,
449 | pre .hljs-lisp .hljs-body {
450 | color: #008080
451 | }
452 |
453 | pre .hljs-regexp {
454 | color: #B68
455 | }
456 |
457 | pre .hljs-class {
458 | color: #458;
459 | font-weight: bold
460 | }
461 |
462 | pre .hljs-symbol,
463 | pre .hljs-ruby .hljs-symbol .hljs-string,
464 | pre .hljs-ruby .hljs-symbol .hljs-keyword,
465 | pre .hljs-ruby .hljs-symbol .hljs-keymethods,
466 | pre .hljs-lisp .hljs-keyword,
467 | pre .hljs-tex .hljs-special,
468 | pre .hljs-input_number {
469 | color: #990073
470 | }
471 |
472 | pre .hljs-builtin,
473 | pre .hljs-constructor,
474 | pre .hljs-built_in,
475 | pre .hljs-lisp .hljs-title {
476 | color: #0086b3
477 | }
478 |
479 | pre .hljs-preprocessor,
480 | pre .hljs-pi,
481 | pre .hljs-doctype,
482 | pre .hljs-shebang,
483 | pre .hljs-cdata {
484 | color: #999;
485 | font-weight: bold
486 | }
487 |
488 | pre .hljs-deletion {
489 | background: #fdd
490 | }
491 |
492 | pre .hljs-addition {
493 | background: #dfd
494 | }
495 |
496 | pre .hljs-diff .hljs-change {
497 | background: #0086b3
498 | }
499 |
500 | pre .hljs-chunk {
501 | color: #aaa
502 | }
503 |
504 | pre .hljs-tex .hljs-formula {
505 | opacity: 0.5;
506 | }
507 |
--------------------------------------------------------------------------------
/docs/exponential.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | exponential.js
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Jump To …
17 | +
18 |
19 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
exponential.js
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
71 |
Copyright (c) 2012 Mathieu Turcotte
72 | Licensed under the MIT license.
73 |
74 |
75 |
76 |
77 | var util = require ('util' );
78 | var precond = require ('precond' );
79 |
80 | var BackoffStrategy = require ('./strategy' );
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
91 |
Exponential backoff strategy.
92 |
93 |
94 |
95 | function ExponentialBackoffStrategy (options) {
96 | BackoffStrategy.call(this , options);
97 | this .backoffDelay_ = 0 ;
98 | this .nextBackoffDelay_ = this .getInitialDelay();
99 | this .factor_ = ExponentialBackoffStrategy.DEFAULT_FACTOR;
100 |
101 | if (options && options.factor !== undefined ) {
102 | precond.checkArgument(options.factor > 1 ,
103 | 'Exponential factor should be greater than 1 but got %s.' ,
104 | options.factor);
105 | this .factor_ = options.factor;
106 | }
107 | }
108 | util.inherits(ExponentialBackoffStrategy, BackoffStrategy);
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
119 |
Default multiplication factor used to compute the next backoff delay from
120 | the current one. The value can be overridden by passing a custom factor as
121 | part of the options.
122 |
123 |
124 |
125 | ExponentialBackoffStrategy.DEFAULT_FACTOR = 2 ;
126 |
127 | ExponentialBackoffStrategy.prototype.next_ = function () {
128 | this .backoffDelay_ = Math .min(this .nextBackoffDelay_, this .getMaxDelay());
129 | this .nextBackoffDelay_ = this .backoffDelay_ * this .factor_;
130 | return this .backoffDelay_;
131 | };
132 |
133 | ExponentialBackoffStrategy.prototype.reset_ = function () {
134 | this .backoffDelay_ = 0 ;
135 | this .nextBackoffDelay_ = this .getInitialDelay();
136 | };
137 |
138 | module.exports = ExponentialBackoffStrategy;
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
--------------------------------------------------------------------------------
/docs/fibonacci.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | fibonacci.js
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Jump To …
17 | +
18 |
19 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
fibonacci.js
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
71 |
Copyright (c) 2012 Mathieu Turcotte
72 | Licensed under the MIT license.
73 |
74 |
75 |
76 |
77 | var util = require ('util' );
78 |
79 | var BackoffStrategy = require ('./strategy' );
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
90 |
Fibonacci backoff strategy.
91 |
92 |
93 |
94 | function FibonacciBackoffStrategy (options) {
95 | BackoffStrategy.call(this , options);
96 | this .backoffDelay_ = 0 ;
97 | this .nextBackoffDelay_ = this .getInitialDelay();
98 | }
99 | util.inherits(FibonacciBackoffStrategy, BackoffStrategy);
100 |
101 | FibonacciBackoffStrategy.prototype.next_ = function () {
102 | var backoffDelay = Math .min(this .nextBackoffDelay_, this .getMaxDelay());
103 | this .nextBackoffDelay_ += this .backoffDelay_;
104 | this .backoffDelay_ = backoffDelay;
105 | return backoffDelay;
106 | };
107 |
108 | FibonacciBackoffStrategy.prototype.reset_ = function () {
109 | this .nextBackoffDelay_ = this .getInitialDelay();
110 | this .backoffDelay_ = 0 ;
111 | };
112 |
113 | module.exports = FibonacciBackoffStrategy;
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
--------------------------------------------------------------------------------
/docs/function_call.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | function_call.js
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Jump To …
17 | +
18 |
19 |
52 |
53 |
54 |
55 |
490 |
491 |
492 |
493 |
--------------------------------------------------------------------------------
/docs/img/backoff_events.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/img/backoff_events.png
--------------------------------------------------------------------------------
/docs/img/function_call_events.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/img/function_call_events.png
--------------------------------------------------------------------------------
/docs/img/layers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/img/layers.png
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | index.js
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Jump To …
17 | +
18 |
19 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
index.js
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
71 |
Copyright (c) 2012 Mathieu Turcotte
72 | Licensed under the MIT license.
73 |
74 |
75 |
76 |
77 | var Backoff = require ('./lib/backoff' );
78 | var ExponentialBackoffStrategy = require ('./lib/strategy/exponential' );
79 | var FibonacciBackoffStrategy = require ('./lib/strategy/fibonacci' );
80 | var FunctionCall = require ('./lib/function_call.js' );
81 |
82 | module.exports.Backoff = Backoff;
83 | module.exports.FunctionCall = FunctionCall;
84 | module.exports.FibonacciStrategy = FibonacciBackoffStrategy;
85 | module.exports.ExponentialStrategy = ExponentialBackoffStrategy;
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
96 |
Constructs a Fibonacci backoff.
97 |
98 |
99 |
100 | module.exports.fibonacci = function (options) {
101 | return new Backoff(new FibonacciBackoffStrategy(options));
102 | };
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
113 |
Constructs an exponential backoff.
114 |
115 |
116 |
117 | module.exports.exponential = function (options) {
118 | return new Backoff(new ExponentialBackoffStrategy(options));
119 | };
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
130 |
Constructs a FunctionCall for the given function and arguments.
131 |
132 |
133 |
134 | module.exports.call = function (fn, vargs, callback) {
135 | var args = Array .prototype.slice.call(arguments );
136 | fn = args[0 ];
137 | vargs = args.slice(1 , args.length - 1 );
138 | callback = args[args.length - 1 ];
139 | return new FunctionCall(fn, vargs, callback);
140 | };
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
--------------------------------------------------------------------------------
/docs/public/fonts/aller-bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/aller-bold.eot
--------------------------------------------------------------------------------
/docs/public/fonts/aller-bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/aller-bold.ttf
--------------------------------------------------------------------------------
/docs/public/fonts/aller-bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/aller-bold.woff
--------------------------------------------------------------------------------
/docs/public/fonts/aller-light.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/aller-light.eot
--------------------------------------------------------------------------------
/docs/public/fonts/aller-light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/aller-light.ttf
--------------------------------------------------------------------------------
/docs/public/fonts/aller-light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/aller-light.woff
--------------------------------------------------------------------------------
/docs/public/fonts/novecento-bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/novecento-bold.eot
--------------------------------------------------------------------------------
/docs/public/fonts/novecento-bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/novecento-bold.ttf
--------------------------------------------------------------------------------
/docs/public/fonts/novecento-bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/novecento-bold.woff
--------------------------------------------------------------------------------
/docs/public/stylesheets/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v2.0.1 | MIT License | git.io/normalize */
2 |
3 | /* ==========================================================================
4 | HTML5 display definitions
5 | ========================================================================== */
6 |
7 | /*
8 | * Corrects `block` display not defined in IE 8/9.
9 | */
10 |
11 | article,
12 | aside,
13 | details,
14 | figcaption,
15 | figure,
16 | footer,
17 | header,
18 | hgroup,
19 | nav,
20 | section,
21 | summary {
22 | display: block;
23 | }
24 |
25 | /*
26 | * Corrects `inline-block` display not defined in IE 8/9.
27 | */
28 |
29 | audio,
30 | canvas,
31 | video {
32 | display: inline-block;
33 | }
34 |
35 | /*
36 | * Prevents modern browsers from displaying `audio` without controls.
37 | * Remove excess height in iOS 5 devices.
38 | */
39 |
40 | audio:not([controls]) {
41 | display: none;
42 | height: 0;
43 | }
44 |
45 | /*
46 | * Addresses styling for `hidden` attribute not present in IE 8/9.
47 | */
48 |
49 | [hidden] {
50 | display: none;
51 | }
52 |
53 | /* ==========================================================================
54 | Base
55 | ========================================================================== */
56 |
57 | /*
58 | * 1. Sets default font family to sans-serif.
59 | * 2. Prevents iOS text size adjust after orientation change, without disabling
60 | * user zoom.
61 | */
62 |
63 | html {
64 | font-family: sans-serif; /* 1 */
65 | -webkit-text-size-adjust: 100%; /* 2 */
66 | -ms-text-size-adjust: 100%; /* 2 */
67 | }
68 |
69 | /*
70 | * Removes default margin.
71 | */
72 |
73 | body {
74 | margin: 0;
75 | }
76 |
77 | /* ==========================================================================
78 | Links
79 | ========================================================================== */
80 |
81 | /*
82 | * Addresses `outline` inconsistency between Chrome and other browsers.
83 | */
84 |
85 | a:focus {
86 | outline: thin dotted;
87 | }
88 |
89 | /*
90 | * Improves readability when focused and also mouse hovered in all browsers.
91 | */
92 |
93 | a:active,
94 | a:hover {
95 | outline: 0;
96 | }
97 |
98 | /* ==========================================================================
99 | Typography
100 | ========================================================================== */
101 |
102 | /*
103 | * Addresses `h1` font sizes within `section` and `article` in Firefox 4+,
104 | * Safari 5, and Chrome.
105 | */
106 |
107 | h1 {
108 | font-size: 2em;
109 | }
110 |
111 | /*
112 | * Addresses styling not present in IE 8/9, Safari 5, and Chrome.
113 | */
114 |
115 | abbr[title] {
116 | border-bottom: 1px dotted;
117 | }
118 |
119 | /*
120 | * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome.
121 | */
122 |
123 | b,
124 | strong {
125 | font-weight: bold;
126 | }
127 |
128 | /*
129 | * Addresses styling not present in Safari 5 and Chrome.
130 | */
131 |
132 | dfn {
133 | font-style: italic;
134 | }
135 |
136 | /*
137 | * Addresses styling not present in IE 8/9.
138 | */
139 |
140 | mark {
141 | background: #ff0;
142 | color: #000;
143 | }
144 |
145 |
146 | /*
147 | * Corrects font family set oddly in Safari 5 and Chrome.
148 | */
149 |
150 | code,
151 | kbd,
152 | pre,
153 | samp {
154 | font-family: monospace, serif;
155 | font-size: 1em;
156 | }
157 |
158 | /*
159 | * Improves readability of pre-formatted text in all browsers.
160 | */
161 |
162 | pre {
163 | white-space: pre;
164 | white-space: pre-wrap;
165 | word-wrap: break-word;
166 | }
167 |
168 | /*
169 | * Sets consistent quote types.
170 | */
171 |
172 | q {
173 | quotes: "\201C" "\201D" "\2018" "\2019";
174 | }
175 |
176 | /*
177 | * Addresses inconsistent and variable font size in all browsers.
178 | */
179 |
180 | small {
181 | font-size: 80%;
182 | }
183 |
184 | /*
185 | * Prevents `sub` and `sup` affecting `line-height` in all browsers.
186 | */
187 |
188 | sub,
189 | sup {
190 | font-size: 75%;
191 | line-height: 0;
192 | position: relative;
193 | vertical-align: baseline;
194 | }
195 |
196 | sup {
197 | top: -0.5em;
198 | }
199 |
200 | sub {
201 | bottom: -0.25em;
202 | }
203 |
204 | /* ==========================================================================
205 | Embedded content
206 | ========================================================================== */
207 |
208 | /*
209 | * Removes border when inside `a` element in IE 8/9.
210 | */
211 |
212 | img {
213 | border: 0;
214 | }
215 |
216 | /*
217 | * Corrects overflow displayed oddly in IE 9.
218 | */
219 |
220 | svg:not(:root) {
221 | overflow: hidden;
222 | }
223 |
224 | /* ==========================================================================
225 | Figures
226 | ========================================================================== */
227 |
228 | /*
229 | * Addresses margin not present in IE 8/9 and Safari 5.
230 | */
231 |
232 | figure {
233 | margin: 0;
234 | }
235 |
236 | /* ==========================================================================
237 | Forms
238 | ========================================================================== */
239 |
240 | /*
241 | * Define consistent border, margin, and padding.
242 | */
243 |
244 | fieldset {
245 | border: 1px solid #c0c0c0;
246 | margin: 0 2px;
247 | padding: 0.35em 0.625em 0.75em;
248 | }
249 |
250 | /*
251 | * 1. Corrects color not being inherited in IE 8/9.
252 | * 2. Remove padding so people aren't caught out if they zero out fieldsets.
253 | */
254 |
255 | legend {
256 | border: 0; /* 1 */
257 | padding: 0; /* 2 */
258 | }
259 |
260 | /*
261 | * 1. Corrects font family not being inherited in all browsers.
262 | * 2. Corrects font size not being inherited in all browsers.
263 | * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome
264 | */
265 |
266 | button,
267 | input,
268 | select,
269 | textarea {
270 | font-family: inherit; /* 1 */
271 | font-size: 100%; /* 2 */
272 | margin: 0; /* 3 */
273 | }
274 |
275 | /*
276 | * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in
277 | * the UA stylesheet.
278 | */
279 |
280 | button,
281 | input {
282 | line-height: normal;
283 | }
284 |
285 | /*
286 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
287 | * and `video` controls.
288 | * 2. Corrects inability to style clickable `input` types in iOS.
289 | * 3. Improves usability and consistency of cursor style between image-type
290 | * `input` and others.
291 | */
292 |
293 | button,
294 | html input[type="button"], /* 1 */
295 | input[type="reset"],
296 | input[type="submit"] {
297 | -webkit-appearance: button; /* 2 */
298 | cursor: pointer; /* 3 */
299 | }
300 |
301 | /*
302 | * Re-set default cursor for disabled elements.
303 | */
304 |
305 | button[disabled],
306 | input[disabled] {
307 | cursor: default;
308 | }
309 |
310 | /*
311 | * 1. Addresses box sizing set to `content-box` in IE 8/9.
312 | * 2. Removes excess padding in IE 8/9.
313 | */
314 |
315 | input[type="checkbox"],
316 | input[type="radio"] {
317 | box-sizing: border-box; /* 1 */
318 | padding: 0; /* 2 */
319 | }
320 |
321 | /*
322 | * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome.
323 | * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome
324 | * (include `-moz` to future-proof).
325 | */
326 |
327 | input[type="search"] {
328 | -webkit-appearance: textfield; /* 1 */
329 | -moz-box-sizing: content-box;
330 | -webkit-box-sizing: content-box; /* 2 */
331 | box-sizing: content-box;
332 | }
333 |
334 | /*
335 | * Removes inner padding and search cancel button in Safari 5 and Chrome
336 | * on OS X.
337 | */
338 |
339 | input[type="search"]::-webkit-search-cancel-button,
340 | input[type="search"]::-webkit-search-decoration {
341 | -webkit-appearance: none;
342 | }
343 |
344 | /*
345 | * Removes inner padding and border in Firefox 4+.
346 | */
347 |
348 | button::-moz-focus-inner,
349 | input::-moz-focus-inner {
350 | border: 0;
351 | padding: 0;
352 | }
353 |
354 | /*
355 | * 1. Removes default vertical scrollbar in IE 8/9.
356 | * 2. Improves readability and alignment in all browsers.
357 | */
358 |
359 | textarea {
360 | overflow: auto; /* 1 */
361 | vertical-align: top; /* 2 */
362 | }
363 |
364 | /* ==========================================================================
365 | Tables
366 | ========================================================================== */
367 |
368 | /*
369 | * Remove most spacing between table cells.
370 | */
371 |
372 | table {
373 | border-collapse: collapse;
374 | border-spacing: 0;
375 | }
--------------------------------------------------------------------------------
/docs/strategy.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | strategy.js
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Jump To …
17 | +
18 |
19 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
strategy.js
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
71 |
Copyright (c) 2012 Mathieu Turcotte
72 | Licensed under the MIT license.
73 |
74 |
75 |
76 |
77 | var events = require ('events' );
78 | var util = require ('util' );
79 |
80 | function isDef (value) {
81 | return value !== undefined && value !== null ;
82 | }
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
93 |
Abstract class defining the skeleton for the backoff strategies. Accepts an
94 | object holding the options for the backoff strategy:
95 |
96 | randomisationFactor
: The randomisation factor which must be between 0
97 | and 1 where 1 equates to a randomization factor of 100% and 0 to no
98 | randomization.
99 | initialDelay
: The backoff initial delay in milliseconds.
100 | maxDelay
: The backoff maximal delay in milliseconds.
101 |
102 |
103 |
104 |
105 | function BackoffStrategy (options) {
106 | options = options || {};
107 |
108 | if (isDef(options.initialDelay) && options.initialDelay < 1 ) {
109 | throw new Error ('The initial timeout must be greater than 0.' );
110 | } else if (isDef(options.maxDelay) && options.maxDelay < 1 ) {
111 | throw new Error ('The maximal timeout must be greater than 0.' );
112 | }
113 |
114 | this .initialDelay_ = options.initialDelay || 100 ;
115 | this .maxDelay_ = options.maxDelay || 10000 ;
116 |
117 | if (this .maxDelay_ <= this .initialDelay_) {
118 | throw new Error ('The maximal backoff delay must be ' +
119 | 'greater than the initial backoff delay.' );
120 | }
121 |
122 | if (isDef(options.randomisationFactor) &&
123 | (options.randomisationFactor < 0 || options.randomisationFactor > 1 )) {
124 | throw new Error ('The randomisation factor must be between 0 and 1.' );
125 | }
126 |
127 | this .randomisationFactor_ = options.randomisationFactor || 0 ;
128 | }
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
139 |
Gets the maximal backoff delay.
140 |
141 |
142 |
143 | BackoffStrategy.prototype.getMaxDelay = function () {
144 | return this .maxDelay_;
145 | };
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
156 |
Gets the initial backoff delay.
157 |
158 |
159 |
160 | BackoffStrategy.prototype.getInitialDelay = function () {
161 | return this .initialDelay_;
162 | };
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
173 |
Template method that computes and returns the next backoff delay in
174 | milliseconds.
175 |
176 |
177 |
178 | BackoffStrategy.prototype.next = function () {
179 | var backoffDelay = this .next_();
180 | var randomisationMultiple = 1 + Math .random() * this .randomisationFactor_;
181 | var randomizedDelay = Math .round(backoffDelay * randomisationMultiple);
182 | return randomizedDelay;
183 | };
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
194 |
Computes and returns the next backoff delay. Intended to be overridden by
195 | subclasses.
196 |
197 |
198 |
199 | BackoffStrategy.prototype.next_ = function () {
200 | throw new Error ('BackoffStrategy.next_() unimplemented.' );
201 | };
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
212 |
Template method that resets the backoff delay to its initial value.
213 |
214 |
215 |
216 | BackoffStrategy.prototype.reset = function () {
217 | this .reset_();
218 | };
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
229 |
Resets the backoff delay to its initial value. Intended to be overridden by
230 | subclasses.
231 |
232 |
233 |
234 | BackoffStrategy.prototype.reset_ = function () {
235 | throw new Error ('BackoffStrategy.reset_() unimplemented.' );
236 | };
237 |
238 | module.exports = BackoffStrategy;
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
--------------------------------------------------------------------------------
/examples/exponential.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var backoff = require('../index');
4 |
5 | var testBackoff = backoff.exponential({
6 | initialDelay: 10,
7 | maxDelay: 1000
8 | });
9 |
10 | testBackoff.on('backoff', function(number, delay) {
11 | console.log('Backoff start: ' + number + ' ' + delay + 'ms');
12 | });
13 |
14 | testBackoff.on('ready', function(number, delay) {
15 | console.log('Backoff done: ' + number + ' ' + delay + 'ms');
16 |
17 | if (number < 15) {
18 | testBackoff.backoff();
19 | }
20 | });
21 |
22 | testBackoff.backoff();
23 |
--------------------------------------------------------------------------------
/examples/exponential_strategy.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var backoff = require('../index');
4 |
5 | var strategy = new backoff.ExponentialStrategy();
6 |
7 | for (var i = 0; i < 10; i++) {
8 | console.log(strategy.next());
9 | }
10 |
--------------------------------------------------------------------------------
/examples/fail.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var backoff = require('../index');
4 |
5 | var testBackoff = backoff.exponential({
6 | initialDelay: 10,
7 | maxDelay: 1000
8 | });
9 |
10 | testBackoff.failAfter(5);
11 |
12 | testBackoff.on('backoff', function(number, delay) {
13 | console.log('Backoff start: ' + number + ' ' + delay + 'ms');
14 | });
15 |
16 | testBackoff.on('ready', function(number, delay) {
17 | console.log('Backoff done: ' + number + ' ' + delay + 'ms');
18 | testBackoff.backoff(); // Launch a new backoff.
19 | });
20 |
21 | testBackoff.on('fail', function() {
22 | console.log('Backoff failure.');
23 | });
24 |
25 | testBackoff.backoff();
26 |
--------------------------------------------------------------------------------
/examples/fibonacci.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var backoff = require('../index');
4 |
5 | var testBackoff = backoff.fibonacci({
6 | initialDelay: 10,
7 | maxDelay: 1000
8 | });
9 |
10 | testBackoff.on('backoff', function(number, delay) {
11 | console.log('Backoff start: ' + number + ' ' + delay + 'ms');
12 | });
13 |
14 | testBackoff.on('ready', function(number, delay) {
15 | console.log('Backoff done: ' + number + ' ' + delay + 'ms');
16 |
17 | if (number < 15) {
18 | testBackoff.backoff();
19 | }
20 | });
21 |
22 | testBackoff.backoff();
23 |
--------------------------------------------------------------------------------
/examples/fibonacci_strategy.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var backoff = require('../index');
4 |
5 | var strategy = new backoff.FibonacciStrategy();
6 |
7 | for (var i = 0; i < 10; i++) {
8 | console.log(strategy.next());
9 | }
10 |
--------------------------------------------------------------------------------
/examples/function_call.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var backoff = require('../index.js'),
4 | util = require('util'),
5 | http = require('http');
6 |
7 | var URL = 'http://www.iana.org/domains/example/';
8 |
9 | function get(options, callback) {
10 | http.get(options, function(res) {
11 | res.setEncoding('utf8');
12 | res.data = '';
13 | res.on('data', function (chunk) {
14 | res.data += chunk;
15 | });
16 | res.on('end', function() {
17 | callback(null, res);
18 | });
19 | res.on('close', function(err) {
20 | callback(err, res);
21 | });
22 | }).on('error', function(err) {
23 | callback(err, null);
24 | });
25 | }
26 |
27 |
28 | var call = backoff.call(get, URL, function(err, res) {
29 | // Notice how the call is captured inside the closure.
30 | console.log('Num retries: ' + call.getNumRetries());
31 |
32 | if (err) {
33 | console.log('Error: ' + err.message);
34 | } else {
35 | console.log('Status: ' + res.statusCode);
36 | }
37 | });
38 |
39 | // Called when function is called with function's args.
40 | call.on('call', function(url) {
41 | console.log('call: ' + util.inspect(arguments));
42 | });
43 |
44 | // Called with results each time function returns.
45 | call.on('callback', function(err, res) {
46 | console.log('callback: ' + util.inspect(arguments));
47 | });
48 |
49 | // Called on backoff.
50 | call.on('backoff', function(number, delay) {
51 | console.log('backoff: ' + util.inspect(arguments));
52 | });
53 |
54 | call.setStrategy(new backoff.ExponentialStrategy());
55 | call.failAfter(2);
56 | call.start();
57 |
--------------------------------------------------------------------------------
/examples/randomized.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var backoff = require('../index');
4 |
5 | var randomizedBackoff = backoff.fibonacci({
6 | randomisationFactor: 0.4,
7 | initialDelay: 10,
8 | maxDelay: 1000
9 | });
10 |
11 | randomizedBackoff.on('backoff', function(number, delay) {
12 | console.log('Backoff start: ' + number + ' ' + delay + 'ms');
13 | });
14 |
15 | randomizedBackoff.on('ready', function(number, delay) {
16 | console.log('Backoff done: ' + number + ' ' + delay + 'ms');
17 |
18 | if (number < 15) {
19 | randomizedBackoff.backoff();
20 | }
21 | });
22 |
23 | randomizedBackoff.backoff();
24 |
--------------------------------------------------------------------------------
/examples/readme.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var backoff = require('../index.js');
4 |
5 | var fibonacciBackoff = backoff.fibonacci({
6 | randomisationFactor: 0,
7 | initialDelay: 10,
8 | maxDelay: 300
9 | });
10 |
11 | fibonacciBackoff.failAfter(10);
12 |
13 | fibonacciBackoff.on('backoff', function(number, delay) {
14 | // Do something when backoff starts, e.g. show to the
15 | // user the delay before next reconnection attempt.
16 | console.log(number + ' ' + delay + 'ms');
17 | });
18 |
19 | fibonacciBackoff.on('ready', function(number, delay) {
20 | // Do something when backoff ends, e.g. retry a failed
21 | // operation (DNS lookup, API call, etc.).
22 | fibonacciBackoff.backoff();
23 | });
24 |
25 | fibonacciBackoff.on('fail', function() {
26 | // Do something when the maximum number of backoffs is
27 | // reached, e.g. ask the user to check its connection.
28 | console.log('fail');
29 | });
30 |
31 | fibonacciBackoff.backoff();
32 |
--------------------------------------------------------------------------------
/examples/reset.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var backoff = require('../index');
4 |
5 | var backoff = backoff.exponential();
6 |
7 | backoff.on('ready', function(number, delay) {
8 | console.log('Backoff done: ' + number + ' ' + delay + 'ms');
9 |
10 | if (number < 15) {
11 | backoff.backoff();
12 | }
13 | });
14 |
15 | backoff.backoff();
16 |
17 | setInterval(function() {
18 | backoff.reset();
19 | backoff.backoff();
20 | }, 5000);
21 |
--------------------------------------------------------------------------------
/examples/set_timeout.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var backoff = require('../index');
4 |
5 | // This example demonstrates how the backoff strategy can be used directly
6 | // to drive a backoff operation using direct calls to setTimeout(fn, delay).
7 |
8 | var strategy = new backoff.ExponentialStrategy({
9 | randomisationFactor: 0.5,
10 | initialDelay: 10,
11 | maxDelay: 1000,
12 | factor: 3
13 | });
14 |
15 | var attempt = 1;
16 |
17 | function doSomething() {
18 | if (attempt > 10) {
19 | console.log('Success!');
20 | strategy.reset();
21 | return;
22 | }
23 |
24 | console.log('Attempt #' + attempt);
25 | attempt++;
26 | setTimeout(doSomething, strategy.next());
27 | }
28 |
29 | doSomething();
30 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2012 Mathieu Turcotte
2 | // Licensed under the MIT license.
3 |
4 | var Backoff = require('./lib/backoff');
5 | var ExponentialBackoffStrategy = require('./lib/strategy/exponential');
6 | var FibonacciBackoffStrategy = require('./lib/strategy/fibonacci');
7 | var FunctionCall = require('./lib/function_call.js');
8 |
9 | module.exports.Backoff = Backoff;
10 | module.exports.FunctionCall = FunctionCall;
11 | module.exports.FibonacciStrategy = FibonacciBackoffStrategy;
12 | module.exports.ExponentialStrategy = ExponentialBackoffStrategy;
13 |
14 | // Constructs a Fibonacci backoff.
15 | module.exports.fibonacci = function(options) {
16 | return new Backoff(new FibonacciBackoffStrategy(options));
17 | };
18 |
19 | // Constructs an exponential backoff.
20 | module.exports.exponential = function(options) {
21 | return new Backoff(new ExponentialBackoffStrategy(options));
22 | };
23 |
24 | // Constructs a FunctionCall for the given function and arguments.
25 | module.exports.call = function(fn, vargs, callback) {
26 | var args = Array.prototype.slice.call(arguments);
27 | fn = args[0];
28 | vargs = args.slice(1, args.length - 1);
29 | callback = args[args.length - 1];
30 | return new FunctionCall(fn, vargs, callback);
31 | };
32 |
--------------------------------------------------------------------------------
/lib/backoff.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2012 Mathieu Turcotte
2 | // Licensed under the MIT license.
3 |
4 | var events = require('events');
5 | var precond = require('precond');
6 | var util = require('util');
7 |
8 | // A class to hold the state of a backoff operation. Accepts a backoff strategy
9 | // to generate the backoff delays.
10 | function Backoff(backoffStrategy) {
11 | events.EventEmitter.call(this);
12 |
13 | this.backoffStrategy_ = backoffStrategy;
14 | this.maxNumberOfRetry_ = -1;
15 | this.backoffNumber_ = 0;
16 | this.backoffDelay_ = 0;
17 | this.timeoutID_ = -1;
18 |
19 | this.handlers = {
20 | backoff: this.onBackoff_.bind(this)
21 | };
22 | }
23 | util.inherits(Backoff, events.EventEmitter);
24 |
25 | // Sets a limit, greater than 0, on the maximum number of backoffs. A 'fail'
26 | // event will be emitted when the limit is reached.
27 | Backoff.prototype.failAfter = function(maxNumberOfRetry) {
28 | precond.checkArgument(maxNumberOfRetry > 0,
29 | 'Expected a maximum number of retry greater than 0 but got %s.',
30 | maxNumberOfRetry);
31 |
32 | this.maxNumberOfRetry_ = maxNumberOfRetry;
33 | };
34 |
35 | // Starts a backoff operation. Accepts an optional parameter to let the
36 | // listeners know why the backoff operation was started.
37 | Backoff.prototype.backoff = function(err) {
38 | precond.checkState(this.timeoutID_ === -1, 'Backoff in progress.');
39 |
40 | if (this.backoffNumber_ === this.maxNumberOfRetry_) {
41 | this.emit('fail', err);
42 | this.reset();
43 | } else {
44 | this.backoffDelay_ = this.backoffStrategy_.next();
45 | this.timeoutID_ = setTimeout(this.handlers.backoff, this.backoffDelay_);
46 | this.emit('backoff', this.backoffNumber_, this.backoffDelay_, err);
47 | }
48 | };
49 |
50 | // Handles the backoff timeout completion.
51 | Backoff.prototype.onBackoff_ = function() {
52 | this.timeoutID_ = -1;
53 | this.emit('ready', this.backoffNumber_, this.backoffDelay_);
54 | this.backoffNumber_++;
55 | };
56 |
57 | // Stops any backoff operation and resets the backoff delay to its inital value.
58 | Backoff.prototype.reset = function() {
59 | this.backoffNumber_ = 0;
60 | this.backoffStrategy_.reset();
61 | clearTimeout(this.timeoutID_);
62 | this.timeoutID_ = -1;
63 | };
64 |
65 | module.exports = Backoff;
66 |
--------------------------------------------------------------------------------
/lib/function_call.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2012 Mathieu Turcotte
2 | // Licensed under the MIT license.
3 |
4 | var events = require('events');
5 | var precond = require('precond');
6 | var util = require('util');
7 |
8 | var Backoff = require('./backoff');
9 | var FibonacciBackoffStrategy = require('./strategy/fibonacci');
10 |
11 | // Wraps a function to be called in a backoff loop.
12 | function FunctionCall(fn, args, callback) {
13 | events.EventEmitter.call(this);
14 |
15 | precond.checkIsFunction(fn, 'Expected fn to be a function.');
16 | precond.checkIsArray(args, 'Expected args to be an array.');
17 | precond.checkIsFunction(callback, 'Expected callback to be a function.');
18 |
19 | this.function_ = fn;
20 | this.arguments_ = args;
21 | this.callback_ = callback;
22 | this.lastResult_ = [];
23 | this.numRetries_ = 0;
24 |
25 | this.backoff_ = null;
26 | this.strategy_ = null;
27 | this.failAfter_ = -1;
28 | this.retryPredicate_ = FunctionCall.DEFAULT_RETRY_PREDICATE_;
29 |
30 | this.state_ = FunctionCall.State_.PENDING;
31 | }
32 | util.inherits(FunctionCall, events.EventEmitter);
33 |
34 | // States in which the call can be.
35 | FunctionCall.State_ = {
36 | // Call isn't started yet.
37 | PENDING: 0,
38 | // Call is in progress.
39 | RUNNING: 1,
40 | // Call completed successfully which means that either the wrapped function
41 | // returned successfully or the maximal number of backoffs was reached.
42 | COMPLETED: 2,
43 | // The call was aborted.
44 | ABORTED: 3
45 | };
46 |
47 | // The default retry predicate which considers any error as retriable.
48 | FunctionCall.DEFAULT_RETRY_PREDICATE_ = function(err) {
49 | return true;
50 | };
51 |
52 | // Checks whether the call is pending.
53 | FunctionCall.prototype.isPending = function() {
54 | return this.state_ == FunctionCall.State_.PENDING;
55 | };
56 |
57 | // Checks whether the call is in progress.
58 | FunctionCall.prototype.isRunning = function() {
59 | return this.state_ == FunctionCall.State_.RUNNING;
60 | };
61 |
62 | // Checks whether the call is completed.
63 | FunctionCall.prototype.isCompleted = function() {
64 | return this.state_ == FunctionCall.State_.COMPLETED;
65 | };
66 |
67 | // Checks whether the call is aborted.
68 | FunctionCall.prototype.isAborted = function() {
69 | return this.state_ == FunctionCall.State_.ABORTED;
70 | };
71 |
72 | // Sets the backoff strategy to use. Can only be called before the call is
73 | // started otherwise an exception will be thrown.
74 | FunctionCall.prototype.setStrategy = function(strategy) {
75 | precond.checkState(this.isPending(), 'FunctionCall in progress.');
76 | this.strategy_ = strategy;
77 | return this; // Return this for chaining.
78 | };
79 |
80 | // Sets the predicate which will be used to determine whether the errors
81 | // returned from the wrapped function should be retried or not, e.g. a
82 | // network error would be retriable while a type error would stop the
83 | // function call.
84 | FunctionCall.prototype.retryIf = function(retryPredicate) {
85 | precond.checkState(this.isPending(), 'FunctionCall in progress.');
86 | this.retryPredicate_ = retryPredicate;
87 | return this;
88 | };
89 |
90 | // Returns all intermediary results returned by the wrapped function since
91 | // the initial call.
92 | FunctionCall.prototype.getLastResult = function() {
93 | return this.lastResult_.concat();
94 | };
95 |
96 | // Returns the number of times the wrapped function call was retried.
97 | FunctionCall.prototype.getNumRetries = function() {
98 | return this.numRetries_;
99 | };
100 |
101 | // Sets the backoff limit.
102 | FunctionCall.prototype.failAfter = function(maxNumberOfRetry) {
103 | precond.checkState(this.isPending(), 'FunctionCall in progress.');
104 | this.failAfter_ = maxNumberOfRetry;
105 | return this; // Return this for chaining.
106 | };
107 |
108 | // Aborts the call.
109 | FunctionCall.prototype.abort = function() {
110 | if (this.isCompleted() || this.isAborted()) {
111 | return;
112 | }
113 |
114 | if (this.isRunning()) {
115 | this.backoff_.reset();
116 | }
117 |
118 | this.state_ = FunctionCall.State_.ABORTED;
119 | this.lastResult_ = [new Error('Backoff aborted.')];
120 | this.emit('abort');
121 | this.doCallback_();
122 | };
123 |
124 | // Initiates the call to the wrapped function. Accepts an optional factory
125 | // function used to create the backoff instance; used when testing.
126 | FunctionCall.prototype.start = function(backoffFactory) {
127 | precond.checkState(!this.isAborted(), 'FunctionCall is aborted.');
128 | precond.checkState(this.isPending(), 'FunctionCall already started.');
129 |
130 | var strategy = this.strategy_ || new FibonacciBackoffStrategy();
131 |
132 | this.backoff_ = backoffFactory ?
133 | backoffFactory(strategy) :
134 | new Backoff(strategy);
135 |
136 | this.backoff_.on('ready', this.doCall_.bind(this, true /* isRetry */));
137 | this.backoff_.on('fail', this.doCallback_.bind(this));
138 | this.backoff_.on('backoff', this.handleBackoff_.bind(this));
139 |
140 | if (this.failAfter_ > 0) {
141 | this.backoff_.failAfter(this.failAfter_);
142 | }
143 |
144 | this.state_ = FunctionCall.State_.RUNNING;
145 | this.doCall_(false /* isRetry */);
146 | };
147 |
148 | // Calls the wrapped function.
149 | FunctionCall.prototype.doCall_ = function(isRetry) {
150 | if (isRetry) {
151 | this.numRetries_++;
152 | }
153 | var eventArgs = ['call'].concat(this.arguments_);
154 | events.EventEmitter.prototype.emit.apply(this, eventArgs);
155 | var callback = this.handleFunctionCallback_.bind(this);
156 | this.function_.apply(null, this.arguments_.concat(callback));
157 | };
158 |
159 | // Calls the wrapped function's callback with the last result returned by the
160 | // wrapped function.
161 | FunctionCall.prototype.doCallback_ = function() {
162 | this.callback_.apply(null, this.lastResult_);
163 | };
164 |
165 | // Handles wrapped function's completion. This method acts as a replacement
166 | // for the original callback function.
167 | FunctionCall.prototype.handleFunctionCallback_ = function() {
168 | if (this.isAborted()) {
169 | return;
170 | }
171 |
172 | var args = Array.prototype.slice.call(arguments);
173 | this.lastResult_ = args; // Save last callback arguments.
174 | events.EventEmitter.prototype.emit.apply(this, ['callback'].concat(args));
175 |
176 | var err = args[0];
177 | if (err && this.retryPredicate_(err)) {
178 | this.backoff_.backoff(err);
179 | } else {
180 | this.state_ = FunctionCall.State_.COMPLETED;
181 | this.doCallback_();
182 | }
183 | };
184 |
185 | // Handles the backoff event by reemitting it.
186 | FunctionCall.prototype.handleBackoff_ = function(number, delay, err) {
187 | this.emit('backoff', number, delay, err);
188 | };
189 |
190 | module.exports = FunctionCall;
191 |
--------------------------------------------------------------------------------
/lib/strategy/exponential.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2012 Mathieu Turcotte
2 | // Licensed under the MIT license.
3 |
4 | var util = require('util');
5 | var precond = require('precond');
6 |
7 | var BackoffStrategy = require('./strategy');
8 |
9 | // Exponential backoff strategy.
10 | function ExponentialBackoffStrategy(options) {
11 | BackoffStrategy.call(this, options);
12 | this.backoffDelay_ = 0;
13 | this.nextBackoffDelay_ = this.getInitialDelay();
14 | this.factor_ = ExponentialBackoffStrategy.DEFAULT_FACTOR;
15 |
16 | if (options && options.factor !== undefined) {
17 | precond.checkArgument(options.factor > 1,
18 | 'Exponential factor should be greater than 1 but got %s.',
19 | options.factor);
20 | this.factor_ = options.factor;
21 | }
22 | }
23 | util.inherits(ExponentialBackoffStrategy, BackoffStrategy);
24 |
25 | // Default multiplication factor used to compute the next backoff delay from
26 | // the current one. The value can be overridden by passing a custom factor as
27 | // part of the options.
28 | ExponentialBackoffStrategy.DEFAULT_FACTOR = 2;
29 |
30 | ExponentialBackoffStrategy.prototype.next_ = function() {
31 | this.backoffDelay_ = Math.min(this.nextBackoffDelay_, this.getMaxDelay());
32 | this.nextBackoffDelay_ = this.backoffDelay_ * this.factor_;
33 | return this.backoffDelay_;
34 | };
35 |
36 | ExponentialBackoffStrategy.prototype.reset_ = function() {
37 | this.backoffDelay_ = 0;
38 | this.nextBackoffDelay_ = this.getInitialDelay();
39 | };
40 |
41 | module.exports = ExponentialBackoffStrategy;
42 |
--------------------------------------------------------------------------------
/lib/strategy/fibonacci.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2012 Mathieu Turcotte
2 | // Licensed under the MIT license.
3 |
4 | var util = require('util');
5 |
6 | var BackoffStrategy = require('./strategy');
7 |
8 | // Fibonacci backoff strategy.
9 | function FibonacciBackoffStrategy(options) {
10 | BackoffStrategy.call(this, options);
11 | this.backoffDelay_ = 0;
12 | this.nextBackoffDelay_ = this.getInitialDelay();
13 | }
14 | util.inherits(FibonacciBackoffStrategy, BackoffStrategy);
15 |
16 | FibonacciBackoffStrategy.prototype.next_ = function() {
17 | var backoffDelay = Math.min(this.nextBackoffDelay_, this.getMaxDelay());
18 | this.nextBackoffDelay_ += this.backoffDelay_;
19 | this.backoffDelay_ = backoffDelay;
20 | return backoffDelay;
21 | };
22 |
23 | FibonacciBackoffStrategy.prototype.reset_ = function() {
24 | this.nextBackoffDelay_ = this.getInitialDelay();
25 | this.backoffDelay_ = 0;
26 | };
27 |
28 | module.exports = FibonacciBackoffStrategy;
29 |
--------------------------------------------------------------------------------
/lib/strategy/strategy.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2012 Mathieu Turcotte
2 | // Licensed under the MIT license.
3 |
4 | var events = require('events');
5 | var util = require('util');
6 |
7 | function isDef(value) {
8 | return value !== undefined && value !== null;
9 | }
10 |
11 | // Abstract class defining the skeleton for the backoff strategies. Accepts an
12 | // object holding the options for the backoff strategy:
13 | //
14 | // * `randomisationFactor`: The randomisation factor which must be between 0
15 | // and 1 where 1 equates to a randomization factor of 100% and 0 to no
16 | // randomization.
17 | // * `initialDelay`: The backoff initial delay in milliseconds.
18 | // * `maxDelay`: The backoff maximal delay in milliseconds.
19 | function BackoffStrategy(options) {
20 | options = options || {};
21 |
22 | if (isDef(options.initialDelay) && options.initialDelay < 1) {
23 | throw new Error('The initial timeout must be greater than 0.');
24 | } else if (isDef(options.maxDelay) && options.maxDelay < 1) {
25 | throw new Error('The maximal timeout must be greater than 0.');
26 | }
27 |
28 | this.initialDelay_ = options.initialDelay || 100;
29 | this.maxDelay_ = options.maxDelay || 10000;
30 |
31 | if (this.maxDelay_ <= this.initialDelay_) {
32 | throw new Error('The maximal backoff delay must be ' +
33 | 'greater than the initial backoff delay.');
34 | }
35 |
36 | if (isDef(options.randomisationFactor) &&
37 | (options.randomisationFactor < 0 || options.randomisationFactor > 1)) {
38 | throw new Error('The randomisation factor must be between 0 and 1.');
39 | }
40 |
41 | this.randomisationFactor_ = options.randomisationFactor || 0;
42 | }
43 |
44 | // Gets the maximal backoff delay.
45 | BackoffStrategy.prototype.getMaxDelay = function() {
46 | return this.maxDelay_;
47 | };
48 |
49 | // Gets the initial backoff delay.
50 | BackoffStrategy.prototype.getInitialDelay = function() {
51 | return this.initialDelay_;
52 | };
53 |
54 | // Template method that computes and returns the next backoff delay in
55 | // milliseconds.
56 | BackoffStrategy.prototype.next = function() {
57 | var backoffDelay = this.next_();
58 | var randomisationMultiple = 1 + Math.random() * this.randomisationFactor_;
59 | var randomizedDelay = Math.round(backoffDelay * randomisationMultiple);
60 | return randomizedDelay;
61 | };
62 |
63 | // Computes and returns the next backoff delay. Intended to be overridden by
64 | // subclasses.
65 | BackoffStrategy.prototype.next_ = function() {
66 | throw new Error('BackoffStrategy.next_() unimplemented.');
67 | };
68 |
69 | // Template method that resets the backoff delay to its initial value.
70 | BackoffStrategy.prototype.reset = function() {
71 | this.reset_();
72 | };
73 |
74 | // Resets the backoff delay to its initial value. Intended to be overridden by
75 | // subclasses.
76 | BackoffStrategy.prototype.reset_ = function() {
77 | throw new Error('BackoffStrategy.reset_() unimplemented.');
78 | };
79 |
80 | module.exports = BackoffStrategy;
81 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backoff",
3 | "description": "Fibonacci and exponential backoffs.",
4 | "version": "2.5.0",
5 | "license": "MIT",
6 | "author": "Mathieu Turcotte ",
7 | "keywords": ["backoff", "retry", "fibonacci", "exponential"],
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/MathieuTurcotte/node-backoff.git"
11 | },
12 | "dependencies": {
13 | "precond": "0.2"
14 | },
15 | "devDependencies": {
16 | "sinon": "1.10",
17 | "nodeunit": "0.9"
18 | },
19 | "scripts": {
20 | "docco" : "docco lib/*.js lib/strategy/* index.js",
21 | "pretest": "jshint lib/ tests/ examples/ index.js",
22 | "test": "node_modules/nodeunit/bin/nodeunit tests/"
23 | },
24 | "engines": {
25 | "node": ">= 0.6"
26 | },
27 | "files": [
28 | "index.js",
29 | "lib",
30 | "tests"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/tests/api.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2012 Mathieu Turcotte
3 | * Licensed under the MIT license.
4 | */
5 |
6 | var sinon = require('sinon');
7 |
8 | var backoff = require('../index');
9 |
10 | exports["API"] = {
11 | "backoff.fibonnaci should be a function that returns a backoff instance": function(test) {
12 | test.ok(backoff.fibonacci, 'backoff.fibonacci should be defined.');
13 | test.equal(typeof backoff.fibonacci, 'function',
14 | 'backoff.fibonacci should be a function.');
15 | test.equal(backoff.fibonacci().constructor.name, 'Backoff');
16 | test.done();
17 | },
18 |
19 | "backoff.exponential should be a function that returns a backoff instance": function(test) {
20 | test.ok(backoff.exponential, 'backoff.exponential should be defined.');
21 | test.equal(typeof backoff.exponential, 'function',
22 | 'backoff.exponential should be a function.');
23 | test.equal(backoff.exponential().constructor.name, 'Backoff');
24 | test.done();
25 | },
26 |
27 | "backoff.call should be a function that returns a FunctionCall instance": function(test) {
28 | var fn = function() {};
29 | var callback = function() {};
30 | test.ok(backoff.Backoff, 'backoff.call should be defined.');
31 | test.equal(typeof backoff.call, 'function',
32 | 'backoff.call should be a function.');
33 | test.equal(backoff.call(fn, 1, 2, 3, callback).constructor.name,
34 | 'FunctionCall');
35 | test.done();
36 | },
37 |
38 | "backoff.Backoff should be defined and a function": function(test) {
39 | test.ok(backoff.Backoff, 'backoff.Backoff should be defined.');
40 | test.equal(typeof backoff.Backoff, 'function',
41 | 'backoff.Backoff should be a function.');
42 | test.done();
43 | },
44 |
45 | "backoff.FunctionCall should be defined and a function": function(test) {
46 | test.ok(backoff.FunctionCall,
47 | 'backoff.FunctionCall should be defined.');
48 | test.equal(typeof backoff.FunctionCall, 'function',
49 | 'backoff.FunctionCall should be a function.');
50 | test.done();
51 | },
52 |
53 | "backoff.FibonacciStrategy should be defined and a function": function(test) {
54 | test.ok(backoff.FibonacciStrategy,
55 | 'backoff.FibonacciStrategy should be defined.');
56 | test.equal(typeof backoff.FibonacciStrategy, 'function',
57 | 'backoff.FibonacciStrategy should be a function.');
58 | test.done();
59 | },
60 |
61 | "backoff.ExponentialStrategy should be defined and a function": function(test) {
62 | test.ok(backoff.ExponentialStrategy,
63 | 'backoff.ExponentialStrategy should be defined.');
64 | test.equal(typeof backoff.ExponentialStrategy, 'function',
65 | 'backoff.ExponentialStrategy should be a function.');
66 | test.done();
67 | }
68 | };
69 |
--------------------------------------------------------------------------------
/tests/backoff.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2012 Mathieu Turcotte
3 | * Licensed under the MIT license.
4 | */
5 |
6 | var sinon = require('sinon');
7 |
8 | var Backoff = require('../lib/backoff');
9 | var BackoffStrategy = require('../lib/strategy/strategy');
10 |
11 | exports["Backoff"] = {
12 | setUp: function(callback) {
13 | this.backoffStrategy = sinon.stub(new BackoffStrategy());
14 | this.backoff = new Backoff(this.backoffStrategy);
15 | this.clock = sinon.useFakeTimers();
16 | this.spy = new sinon.spy();
17 | callback();
18 | },
19 |
20 | tearDown: function(callback) {
21 | this.clock.restore();
22 | callback();
23 | },
24 |
25 | "the backoff event should be emitted when backoff starts": function(test) {
26 | this.backoffStrategy.next.returns(10);
27 | this.backoff.on('backoff', this.spy);
28 |
29 | this.backoff.backoff();
30 |
31 | test.ok(this.spy.calledOnce,
32 | 'Backoff event should be emitted when backoff starts.');
33 | test.done();
34 | },
35 |
36 | "the ready event should be emitted on backoff completion": function(test) {
37 | this.backoffStrategy.next.returns(10);
38 | this.backoff.on('ready', this.spy);
39 |
40 | this.backoff.backoff();
41 | this.clock.tick(10);
42 |
43 | test.ok(this.spy.calledOnce,
44 | 'Ready event should be emitted when backoff ends.');
45 | test.done();
46 | },
47 |
48 | "the backoff event should be passed the backoff delay": function(test) {
49 | this.backoffStrategy.next.returns(989);
50 | this.backoff.on('backoff', this.spy);
51 |
52 | this.backoff.backoff();
53 |
54 | test.equal(this.spy.getCall(0).args[1], 989, 'Backoff event should ' +
55 | 'carry the backoff delay as its second argument.');
56 | test.done();
57 | },
58 |
59 | "the ready event should be passed the backoff delay": function(test) {
60 | this.backoffStrategy.next.returns(989);
61 | this.backoff.on('ready', this.spy);
62 |
63 | this.backoff.backoff();
64 | this.clock.tick(989);
65 |
66 | test.equal(this.spy.getCall(0).args[1], 989, 'Ready event should ' +
67 | 'carry the backoff delay as its second argument.');
68 | test.done();
69 | },
70 |
71 | "the fail event should be emitted when backoff limit is reached": function(test) {
72 | var err = new Error('Fail');
73 |
74 | this.backoffStrategy.next.returns(10);
75 | this.backoff.on('fail', this.spy);
76 |
77 | this.backoff.failAfter(2);
78 |
79 | // Consume first 2 backoffs.
80 | for (var i = 0; i < 2; i++) {
81 | this.backoff.backoff();
82 | this.clock.tick(10);
83 | }
84 |
85 | // Failure should occur on the third call, and not before.
86 | test.ok(!this.spy.calledOnce, 'Fail event shouldn\'t have been emitted.');
87 | this.backoff.backoff(err);
88 | test.ok(this.spy.calledOnce, 'Fail event should have been emitted.');
89 | test.equal(this.spy.getCall(0).args[0], err, 'Error should be passed');
90 |
91 | test.done();
92 | },
93 |
94 | "calling backoff while a backoff is in progress should throw an error": function(test) {
95 | this.backoffStrategy.next.returns(10);
96 | var backoff = this.backoff;
97 |
98 | backoff.backoff();
99 |
100 | test.throws(function() {
101 | backoff.backoff();
102 | }, /in progress/);
103 |
104 | test.done();
105 | },
106 |
107 | "backoff limit should be greater than 0": function(test) {
108 | var backoff = this.backoff;
109 | test.throws(function() {
110 | backoff.failAfter(0);
111 | }, /greater than 0 but got 0/);
112 | test.done();
113 | },
114 |
115 | "reset should cancel any backoff in progress": function(test) {
116 | this.backoffStrategy.next.returns(10);
117 | this.backoff.on('ready', this.spy);
118 |
119 | this.backoff.backoff();
120 |
121 | this.backoff.reset();
122 | this.clock.tick(100); // 'ready' should not be emitted.
123 |
124 | test.equals(this.spy.callCount, 0, 'Reset should have aborted the backoff.');
125 | test.done();
126 | },
127 |
128 | "reset should reset the backoff strategy": function(test) {
129 | this.backoff.reset();
130 | test.ok(this.backoffStrategy.reset.calledOnce,
131 | 'The backoff strategy should have been resetted.');
132 | test.done();
133 | },
134 |
135 | "backoff should be reset after fail": function(test) {
136 | this.backoffStrategy.next.returns(10);
137 |
138 | this.backoff.failAfter(1);
139 |
140 | this.backoff.backoff();
141 | this.clock.tick(10);
142 | this.backoff.backoff();
143 |
144 | test.ok(this.backoffStrategy.reset.calledOnce,
145 | 'Backoff should have been resetted after failure.');
146 | test.done();
147 | },
148 |
149 | "the backoff number should increase from 0 to N - 1": function(test) {
150 | this.backoffStrategy.next.returns(10);
151 | this.backoff.on('backoff', this.spy);
152 |
153 | var expectedNumbers = [0, 1, 2, 3, 4];
154 | var actualNumbers = [];
155 |
156 | for (var i = 0; i < expectedNumbers.length; i++) {
157 | this.backoff.backoff();
158 | this.clock.tick(10);
159 | actualNumbers.push(this.spy.getCall(i).args[0]);
160 | }
161 |
162 | test.deepEqual(expectedNumbers, actualNumbers,
163 | 'Backoff number should increase from 0 to N - 1.');
164 | test.done();
165 | }
166 | };
167 |
--------------------------------------------------------------------------------
/tests/backoff_strategy.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2012 Mathieu Turcotte
3 | * Licensed under the MIT license.
4 | */
5 |
6 | var sinon = require('sinon');
7 | var util = require('util');
8 |
9 | var BackoffStrategy = require('../lib/strategy/strategy');
10 |
11 | function SampleBackoffStrategy(options) {
12 | BackoffStrategy.call(this, options);
13 | }
14 | util.inherits(SampleBackoffStrategy, BackoffStrategy);
15 |
16 | SampleBackoffStrategy.prototype.next_ = function() {
17 | return this.getInitialDelay();
18 | };
19 |
20 | SampleBackoffStrategy.prototype.reset_ = function() {};
21 |
22 | exports["BackoffStrategy"] = {
23 | setUp: function(callback) {
24 | this.random = sinon.stub(Math, 'random');
25 | callback();
26 | },
27 |
28 | tearDown: function(callback) {
29 | this.random.restore();
30 | callback();
31 | },
32 |
33 | "the randomisation factor should be between 0 and 1": function(test) {
34 | test.throws(function() {
35 | new BackoffStrategy({
36 | randomisationFactor: -0.1
37 | });
38 | });
39 |
40 | test.throws(function() {
41 | new BackoffStrategy({
42 | randomisationFactor: 1.1
43 | });
44 | });
45 |
46 | test.doesNotThrow(function() {
47 | new BackoffStrategy({
48 | randomisationFactor: 0.5
49 | });
50 | });
51 |
52 | test.done();
53 | },
54 |
55 | "the raw delay should be randomized based on the randomisation factor": function(test) {
56 | var strategy = new SampleBackoffStrategy({
57 | randomisationFactor: 0.5,
58 | initialDelay: 1000
59 | });
60 | this.random.returns(0.5);
61 |
62 | var backoffDelay = strategy.next();
63 |
64 | test.equals(backoffDelay, 1000 + (1000 * 0.5 * 0.5));
65 | test.done();
66 | },
67 |
68 | "the initial backoff delay should be greater than 0": function(test) {
69 | test.throws(function() {
70 | new BackoffStrategy({
71 | initialDelay: -1
72 | });
73 | });
74 |
75 | test.throws(function() {
76 | new BackoffStrategy({
77 | initialDelay: 0
78 | });
79 | });
80 |
81 | test.doesNotThrow(function() {
82 | new BackoffStrategy({
83 | initialDelay: 1
84 | });
85 | });
86 |
87 | test.done();
88 | },
89 |
90 | "the maximal backoff delay should be greater than 0": function(test) {
91 | test.throws(function() {
92 | new BackoffStrategy({
93 | maxDelay: -1
94 | });
95 | });
96 |
97 | test.throws(function() {
98 | new BackoffStrategy({
99 | maxDelay: 0
100 | });
101 | });
102 |
103 | test.done();
104 | },
105 |
106 | "the maximal backoff delay should be greater than the initial backoff delay": function(test) {
107 | test.throws(function() {
108 | new BackoffStrategy({
109 | initialDelay: 10,
110 | maxDelay: 10
111 | });
112 | });
113 |
114 | test.doesNotThrow(function() {
115 | new BackoffStrategy({
116 | initialDelay: 10,
117 | maxDelay: 11
118 | });
119 | });
120 |
121 | test.done();
122 | }
123 | };
124 |
--------------------------------------------------------------------------------
/tests/exponential_backoff_strategy.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2012 Mathieu Turcotte
3 | * Licensed under the MIT license.
4 | */
5 |
6 | var sinon = require('sinon');
7 |
8 | var ExponentialBackoffStrategy = require('../lib/strategy/exponential');
9 |
10 | exports["ExponentialBackoffStrategy"] = {
11 |
12 | "backoff delays should follow an exponential sequence": function(test) {
13 | var strategy = new ExponentialBackoffStrategy({
14 | initialDelay: 10,
15 | maxDelay: 1000
16 | });
17 |
18 | // Exponential sequence: x[i] = x[i-1] * 2.
19 | var expectedDelays = [10, 20, 40, 80, 160, 320, 640, 1000, 1000];
20 | var actualDelays = expectedDelays.map(function () {
21 | return strategy.next();
22 | });
23 |
24 | test.deepEqual(expectedDelays, actualDelays,
25 | 'Generated delays should follow an exponential sequence.');
26 | test.done();
27 | },
28 |
29 | "backoff delay factor should be configurable": function (test) {
30 | var strategy = new ExponentialBackoffStrategy({
31 | initialDelay: 10,
32 | maxDelay: 270,
33 | factor: 3
34 | });
35 |
36 | // Exponential sequence: x[i] = x[i-1] * 3.
37 | var expectedDelays = [10, 30, 90, 270, 270];
38 | var actualDelays = expectedDelays.map(function () {
39 | return strategy.next();
40 | });
41 |
42 | test.deepEqual(expectedDelays, actualDelays,
43 | 'Generated delays should follow a configurable exponential sequence.');
44 | test.done();
45 | },
46 |
47 | "backoff delays should restart from the initial delay after reset": function(test) {
48 | var strategy = new ExponentialBackoffStrategy({
49 | initialDelay: 10,
50 | maxDelay: 1000
51 | });
52 |
53 | strategy.next();
54 | strategy.reset();
55 |
56 | var backoffDelay = strategy.next();
57 | test.equals(backoffDelay, 10,
58 | 'Strategy should return the initial delay after reset.');
59 | test.done();
60 | }
61 | };
62 |
--------------------------------------------------------------------------------
/tests/fibonacci_backoff_strategy.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2012 Mathieu Turcotte
3 | * Licensed under the MIT license.
4 | */
5 |
6 | var sinon = require('sinon');
7 |
8 | var FibonacciBackoffStrategy = require('../lib/strategy/fibonacci');
9 |
10 | exports["FibonacciBackoffStrategy"] = {
11 | setUp: function(callback) {
12 | this.strategy = new FibonacciBackoffStrategy({
13 | initialDelay: 10,
14 | maxDelay: 1000
15 | });
16 | callback();
17 | },
18 |
19 | "backoff delays should follow a Fibonacci sequence": function(test) {
20 | // Fibonacci sequence: x[i] = x[i-1] + x[i-2].
21 | var expectedDelays = [10, 10, 20, 30, 50, 80, 130, 210, 340, 550, 890, 1000];
22 | var actualDelays = [];
23 |
24 | for (var i = 0; i < expectedDelays.length; i++) {
25 | actualDelays.push(this.strategy.next());
26 | }
27 |
28 | test.deepEqual(expectedDelays, actualDelays,
29 | 'Generated delays should follow a Fibonacci sequence.');
30 | test.done();
31 | },
32 |
33 | "backoff delays should restart from the initial delay after reset": function(test) {
34 | var strategy = new FibonacciBackoffStrategy({
35 | initialDelay: 10,
36 | maxDelay: 1000
37 | });
38 |
39 | strategy.next();
40 | strategy.reset();
41 |
42 | var backoffDelay = strategy.next();
43 | test.equals(backoffDelay, 10,
44 | 'Strategy should return the initial delay after reset.');
45 | test.done();
46 | }
47 | };
48 |
--------------------------------------------------------------------------------
/tests/function_call.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2012 Mathieu Turcotte
3 | * Licensed under the MIT license.
4 | */
5 |
6 | var assert = require('assert');
7 | var events = require('events');
8 | var sinon = require('sinon');
9 | var util = require('util');
10 |
11 | var FunctionCall = require('../lib/function_call');
12 |
13 | function MockBackoff() {
14 | events.EventEmitter.call(this);
15 |
16 | this.reset = sinon.spy();
17 | this.backoff = sinon.spy();
18 | this.failAfter = sinon.spy();
19 | }
20 | util.inherits(MockBackoff, events.EventEmitter);
21 |
22 | exports["FunctionCall"] = {
23 | setUp: function(callback) {
24 | this.wrappedFn = sinon.stub();
25 | this.callback = sinon.stub();
26 | this.backoff = new MockBackoff();
27 | this.backoffFactory = sinon.stub();
28 | this.backoffFactory.returns(this.backoff);
29 | callback();
30 | },
31 |
32 | tearDown: function(callback) {
33 | callback();
34 | },
35 |
36 | "constructor's first argument should be a function": function(test) {
37 | test.throws(function() {
38 | new FunctionCall(1, [], function() {});
39 | }, /Expected fn to be a function./);
40 | test.done();
41 | },
42 |
43 | "constructor's last argument should be a function": function(test) {
44 | test.throws(function() {
45 | new FunctionCall(function() {}, [], 3);
46 | }, /Expected callback to be a function./);
47 | test.done();
48 | },
49 |
50 | "isPending should return false once the call is started": function(test) {
51 | this.wrappedFn.
52 | onFirstCall().yields(new Error()).
53 | onSecondCall().yields(null, 'Success!');
54 | var call = new FunctionCall(this.wrappedFn, [], this.callback);
55 |
56 | test.ok(call.isPending());
57 |
58 | call.start(this.backoffFactory);
59 | test.ok(!call.isPending());
60 |
61 | this.backoff.emit('ready');
62 | test.ok(!call.isPending());
63 |
64 | test.done();
65 | },
66 |
67 | "isRunning should return true when call is in progress": function(test) {
68 | this.wrappedFn.
69 | onFirstCall().yields(new Error()).
70 | onSecondCall().yields(null, 'Success!');
71 | var call = new FunctionCall(this.wrappedFn, [], this.callback);
72 |
73 | test.ok(!call.isRunning());
74 |
75 | call.start(this.backoffFactory);
76 | test.ok(call.isRunning());
77 |
78 | this.backoff.emit('ready');
79 | test.ok(!call.isRunning());
80 |
81 | test.done();
82 | },
83 |
84 | "isCompleted should return true once the call completes": function(test) {
85 | this.wrappedFn.
86 | onFirstCall().yields(new Error()).
87 | onSecondCall().yields(null, 'Success!');
88 | var call = new FunctionCall(this.wrappedFn, [], this.callback);
89 |
90 | test.ok(!call.isCompleted());
91 |
92 | call.start(this.backoffFactory);
93 | test.ok(!call.isCompleted());
94 |
95 | this.backoff.emit('ready');
96 | test.ok(call.isCompleted());
97 |
98 | test.done();
99 | },
100 |
101 | "isAborted should return true once the call is aborted": function(test) {
102 | this.wrappedFn.
103 | onFirstCall().yields(new Error()).
104 | onSecondCall().yields(null, 'Success!');
105 | var call = new FunctionCall(this.wrappedFn, [], this.callback);
106 |
107 | test.ok(!call.isAborted());
108 | call.abort();
109 | test.ok(call.isAborted());
110 |
111 | test.done();
112 | },
113 |
114 | "setStrategy should overwrite the default strategy": function(test) {
115 | var replacementStrategy = {};
116 | var call = new FunctionCall(this.wrappedFn, [], this.callback);
117 | call.setStrategy(replacementStrategy);
118 | call.start(this.backoffFactory);
119 | test.ok(this.backoffFactory.calledWith(replacementStrategy),
120 | 'User defined strategy should be used to instantiate ' +
121 | 'the backoff instance.');
122 | test.done();
123 | },
124 |
125 | "setStrategy should throw if the call is in progress": function(test) {
126 | var call = new FunctionCall(this.wrappedFn, [], this.callback);
127 | call.start(this.backoffFactory);
128 | test.throws(function() {
129 | call.setStrategy({});
130 | }, /in progress/);
131 | test.done();
132 | },
133 |
134 | "failAfter should not be set by default": function(test) {
135 | var call = new FunctionCall(this.wrappedFn, [], this.callback);
136 | call.start(this.backoffFactory);
137 | test.equal(0, this.backoff.failAfter.callCount);
138 | test.done();
139 | },
140 |
141 | "failAfter should be used as the maximum number of backoffs": function(test) {
142 | var failAfterValue = 99;
143 | var call = new FunctionCall(this.wrappedFn, [], this.callback);
144 | call.failAfter(failAfterValue);
145 | call.start(this.backoffFactory);
146 | test.ok(this.backoff.failAfter.calledWith(failAfterValue),
147 | 'User defined maximum number of backoffs shoud be ' +
148 | 'used to configure the backoff instance.');
149 | test.done();
150 | },
151 |
152 | "failAfter should throw if the call is in progress": function(test) {
153 | var call = new FunctionCall(this.wrappedFn, [], this.callback);
154 | call.start(this.backoffFactory);
155 | test.throws(function() {
156 | call.failAfter(1234);
157 | }, /in progress/);
158 | test.done();
159 | },
160 |
161 | "start shouldn't allow overlapping invocation": function(test) {
162 | var call = new FunctionCall(this.wrappedFn, [], this.callback);
163 | var backoffFactory = this.backoffFactory;
164 |
165 | call.start(backoffFactory);
166 | test.throws(function() {
167 | call.start(backoffFactory);
168 | }, /already started/);
169 | test.done();
170 | },
171 |
172 | "start shouldn't allow invocation of aborted call": function(test) {
173 | var call = new FunctionCall(this.wrappedFn, [], this.callback);
174 | var backoffFactory = this.backoffFactory;
175 |
176 | call.abort();
177 | test.throws(function() {
178 | call.start(backoffFactory);
179 | }, /aborted/);
180 | test.done();
181 | },
182 |
183 | "call should forward its arguments to the wrapped function": function(test) {
184 | var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
185 | call.start(this.backoffFactory);
186 | test.ok(this.wrappedFn.calledWith(1, 2, 3));
187 | test.done();
188 | },
189 |
190 | "call should complete when the wrapped function succeeds": function(test) {
191 | var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
192 | this.wrappedFn.
193 | onCall(0).yields(new Error()).
194 | onCall(1).yields(new Error()).
195 | onCall(2).yields(new Error()).
196 | onCall(3).yields(null, 'Success!');
197 |
198 | call.start(this.backoffFactory);
199 |
200 | for (var i = 0; i < 2; i++) {
201 | this.backoff.emit('ready');
202 | }
203 |
204 | test.equals(this.callback.callCount, 0);
205 | this.backoff.emit('ready');
206 |
207 | test.ok(this.callback.calledWith(null, 'Success!'));
208 | test.ok(this.wrappedFn.alwaysCalledWith(1, 2, 3));
209 | test.done();
210 | },
211 |
212 | "call should fail when the backoff limit is reached": function(test) {
213 | var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
214 | var error = new Error();
215 | this.wrappedFn.yields(error);
216 | call.start(this.backoffFactory);
217 |
218 | for (var i = 0; i < 3; i++) {
219 | this.backoff.emit('ready');
220 | }
221 |
222 | test.equals(this.callback.callCount, 0);
223 |
224 | this.backoff.emit('fail');
225 |
226 | test.ok(this.callback.calledWith(error));
227 | test.ok(this.wrappedFn.alwaysCalledWith(1, 2, 3));
228 | test.done();
229 | },
230 |
231 | "call should fail when the retry predicate returns false": function(test) {
232 | var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
233 | call.retryIf(function(err) { return err.retriable; });
234 |
235 | var retriableError = new Error();
236 | retriableError.retriable = true;
237 |
238 | var fatalError = new Error();
239 | fatalError.retriable = false;
240 |
241 | this.wrappedFn.
242 | onCall(0).yields(retriableError).
243 | onCall(1).yields(retriableError).
244 | onCall(2).yields(fatalError);
245 |
246 | call.start(this.backoffFactory);
247 |
248 | for (var i = 0; i < 2; i++) {
249 | this.backoff.emit('ready');
250 | }
251 |
252 | test.equals(this.callback.callCount, 1);
253 | test.ok(this.callback.calledWith(fatalError));
254 | test.ok(this.wrappedFn.alwaysCalledWith(1, 2, 3));
255 | test.done();
256 | },
257 |
258 | "wrapped function's callback shouldn't be called after abort": function(test) {
259 | var call = new FunctionCall(function(callback) {
260 | call.abort(); // Abort in middle of wrapped function's execution.
261 | callback(null, 'ok');
262 | }, [], this.callback);
263 |
264 | call.start(this.backoffFactory);
265 |
266 | test.equals(this.callback.callCount, 1,
267 | 'Wrapped function\'s callback shouldn\'t be called after abort.');
268 | test.ok(this.callback.calledWithMatch(sinon.match(function (err) {
269 | return !!err.message.match(/Backoff aborted/);
270 | }, "abort error")));
271 | test.done();
272 | },
273 |
274 | "abort event is emitted once when abort is called": function(test) {
275 | var call = new FunctionCall(this.wrappedFn, [], this.callback);
276 | this.wrappedFn.yields(new Error());
277 | var callEventSpy = sinon.spy();
278 |
279 | call.on('abort', callEventSpy);
280 | call.start(this.backoffFactory);
281 |
282 | call.abort();
283 | call.abort();
284 | call.abort();
285 |
286 | test.equals(callEventSpy.callCount, 1);
287 | test.done();
288 | },
289 |
290 | "getLastResult should return the last intermediary result": function(test) {
291 | var call = new FunctionCall(this.wrappedFn, [], this.callback);
292 | this.wrappedFn.yields(1);
293 | call.start(this.backoffFactory);
294 |
295 | for (var i = 2; i < 5; i++) {
296 | this.wrappedFn.yields(i);
297 | this.backoff.emit('ready');
298 | test.deepEqual([i], call.getLastResult());
299 | }
300 |
301 | this.wrappedFn.yields(null);
302 | this.backoff.emit('ready');
303 | test.deepEqual([null], call.getLastResult());
304 |
305 | test.done();
306 | },
307 |
308 | "getNumRetries should return the number of retries": function(test) {
309 | var call = new FunctionCall(this.wrappedFn, [], this.callback);
310 |
311 | this.wrappedFn.yields(1);
312 | call.start(this.backoffFactory);
313 | // The inital call doesn't count as a retry.
314 | test.equals(0, call.getNumRetries());
315 |
316 | for (var i = 2; i < 5; i++) {
317 | this.wrappedFn.yields(i);
318 | this.backoff.emit('ready');
319 | test.equals(i - 1, call.getNumRetries());
320 | }
321 |
322 | this.wrappedFn.yields(null);
323 | this.backoff.emit('ready');
324 | test.equals(4, call.getNumRetries());
325 |
326 | test.done();
327 | },
328 |
329 | "wrapped function's errors should be propagated": function(test) {
330 | var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
331 | this.wrappedFn.throws(new Error());
332 | test.throws(function() {
333 | call.start(this.backoffFactory);
334 | }, Error);
335 | test.done();
336 | },
337 |
338 | "wrapped callback's errors should be propagated": function(test) {
339 | var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
340 | this.wrappedFn.yields(null, 'Success!');
341 | this.callback.throws(new Error());
342 | test.throws(function() {
343 | call.start(this.backoffFactory);
344 | }, Error);
345 | test.done();
346 | },
347 |
348 | "call event should be emitted when wrapped function gets called": function(test) {
349 | this.wrappedFn.yields(1);
350 | var callEventSpy = sinon.spy();
351 |
352 | var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback);
353 | call.on('call', callEventSpy);
354 | call.start(this.backoffFactory);
355 |
356 | for (var i = 1; i < 5; i++) {
357 | this.backoff.emit('ready');
358 | }
359 |
360 | test.equal(5, callEventSpy.callCount,
361 | 'The call event should have been emitted 5 times.');
362 | test.deepEqual([1, 'two'], callEventSpy.getCall(0).args,
363 | 'The call event should carry function\'s args.');
364 | test.done();
365 | },
366 |
367 | "callback event should be emitted when callback is called": function(test) {
368 | var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback);
369 | var callbackSpy = sinon.spy();
370 | call.on('callback', callbackSpy);
371 |
372 | this.wrappedFn.yields('error');
373 | call.start(this.backoffFactory);
374 |
375 | this.wrappedFn.yields(null, 'done');
376 | this.backoff.emit('ready');
377 |
378 | test.equal(2, callbackSpy.callCount,
379 | 'Callback event should have been emitted 2 times.');
380 | test.deepEqual(['error'], callbackSpy.firstCall.args,
381 | 'First callback event should carry first call\'s results.');
382 | test.deepEqual([null, 'done'], callbackSpy.secondCall.args,
383 | 'Second callback event should carry second call\'s results.');
384 | test.done();
385 | },
386 |
387 | "backoff event should be emitted on backoff start": function(test) {
388 | var err = new Error('backoff event error');
389 | var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback);
390 | var backoffSpy = sinon.spy();
391 |
392 | call.on('backoff', backoffSpy);
393 |
394 | this.wrappedFn.yields(err);
395 | call.start(this.backoffFactory);
396 | this.backoff.emit('backoff', 3, 1234, err);
397 |
398 | test.ok(this.backoff.backoff.calledWith(err),
399 | 'The backoff instance should have been called with the error.');
400 | test.equal(1, backoffSpy.callCount,
401 | 'Backoff event should have been emitted 1 time.');
402 | test.deepEqual([3, 1234, err], backoffSpy.firstCall.args,
403 | 'Backoff event should carry the backoff number, delay and error.');
404 | test.done();
405 | }
406 | };
407 |
--------------------------------------------------------------------------------