var events = require('events');
+var util = require('util');
diff --git a/docs/backoff.html b/docs/backoff.html new file mode 100644 index 0000000..4510588 --- /dev/null +++ b/docs/backoff.html @@ -0,0 +1,206 @@ + + + +
+ Copyright (c) 2012 Mathieu Turcotte
+ Licensed under the MIT license.
+
+ var events = require('events');
+var util = require('util');
A class to hold the state of a backoff operation. Accepts a backoff strategy +to generate the backoff delays.
+ +function Backoff(backoffStrategy) {
+ events.EventEmitter.call(this);
+
+ this.backoffStrategy_ = backoffStrategy;
+ this.maxNumberOfRetry_ = -1;
+ this.backoffNumber_ = 0;
+ this.backoffDelay_ = 0;
+ this.timeoutID_ = -1;
+
+ this.handlers = {
+ backoff: this.onBackoff_.bind(this)
+ };
+}
+util.inherits(Backoff, events.EventEmitter);
Sets a limit, greater than 0, on the maximum number of backoffs. A 'fail' +event will be emitted when the limit is reached.
+ +Backoff.prototype.failAfter = function(maxNumberOfRetry) {
+ if (maxNumberOfRetry < 1) {
+ throw new Error('Maximum number of retry must be greater than 0. ' +
+ 'Actual: ' + maxNumberOfRetry);
+ }
+
+ this.maxNumberOfRetry_ = maxNumberOfRetry;
+};
Starts a backoff operation. Accepts an optional parameter to let the +listeners know why the backoff operation was started.
+ +Backoff.prototype.backoff = function(err) {
+ if (this.timeoutID_ !== -1) {
+ throw new Error('Backoff in progress.');
+ }
+
+ if (this.backoffNumber_ === this.maxNumberOfRetry_) {
+ this.emit('fail', err);
+ this.reset();
+ } else {
+ this.backoffDelay_ = this.backoffStrategy_.next();
+ this.timeoutID_ = setTimeout(this.handlers.backoff, this.backoffDelay_);
+ this.emit('backoff', this.backoffNumber_, this.backoffDelay_, err);
+ }
+};
Handles the backoff timeout completion.
+ +Backoff.prototype.onBackoff_ = function() {
+ this.timeoutID_ = -1;
+ this.emit('ready', this.backoffNumber_, this.backoffDelay_);
+ this.backoffNumber_++;
+};
Stops any backoff operation and resets the backoff delay to its inital value.
+ +Backoff.prototype.reset = function() {
+ this.backoffNumber_ = 0;
+ this.backoffStrategy_.reset();
+ clearTimeout(this.timeoutID_);
+ this.timeoutID_ = -1;
+};
+
+module.exports = Backoff;
Copyright (c) 2012 Mathieu Turcotte
+ Licensed under the MIT license.
+
+ var util = require('util');
+
+var BackoffStrategy = require('./strategy');
Exponential backoff strategy.
+ +function ExponentialBackoffStrategy(options) {
+ BackoffStrategy.call(this, options);
+ this.backoffDelay_ = 0;
+ this.nextBackoffDelay_ = this.getInitialDelay();
+}
+util.inherits(ExponentialBackoffStrategy, BackoffStrategy);
+
+ExponentialBackoffStrategy.prototype.next_ = function() {
+ this.backoffDelay_ = Math.min(this.nextBackoffDelay_, this.getMaxDelay());
+ this.nextBackoffDelay_ = this.backoffDelay_ * 2;
+ return this.backoffDelay_;
+};
+
+ExponentialBackoffStrategy.prototype.reset_ = function() {
+ this.backoffDelay_ = 0;
+ this.nextBackoffDelay_ = this.getInitialDelay();
+};
+
+module.exports = ExponentialBackoffStrategy;
Copyright (c) 2012 Mathieu Turcotte
+ Licensed under the MIT license.
+
+ var util = require('util');
+
+var BackoffStrategy = require('./strategy');
Fibonacci backoff strategy.
+ +function FibonacciBackoffStrategy(options) {
+ BackoffStrategy.call(this, options);
+ this.backoffDelay_ = 0;
+ this.nextBackoffDelay_ = this.getInitialDelay();
+}
+util.inherits(FibonacciBackoffStrategy, BackoffStrategy);
+
+FibonacciBackoffStrategy.prototype.next_ = function() {
+ var backoffDelay = Math.min(this.nextBackoffDelay_, this.getMaxDelay());
+ this.nextBackoffDelay_ += this.backoffDelay_;
+ this.backoffDelay_ = backoffDelay;
+ return backoffDelay;
+};
+
+FibonacciBackoffStrategy.prototype.reset_ = function() {
+ this.nextBackoffDelay_ = this.getInitialDelay();
+ this.backoffDelay_ = 0;
+};
+
+module.exports = FibonacciBackoffStrategy;
Copyright (c) 2012 Mathieu Turcotte
+ Licensed under the MIT license.
+
+ var events = require('events');
+var util = require('util');
+
+var Backoff = require('./backoff');
+var FibonacciBackoffStrategy = require('./strategy/fibonacci');
Checks whether the given value is a function.
+ +function isFunction(val) {
+ return typeof val == 'function';
+}
Wraps a function to be called in a backoff loop.
+ +function FunctionCall(fn, args, callback) {
+ events.EventEmitter.call(this);
+
+ if (!isFunction(fn)) {
+ throw new Error('fn should be a function.' +
+ 'Actual: ' + typeof fn);
+ }
+
+ if (!isFunction(callback)) {
+ throw new Error('callback should be a function.' +
+ 'Actual: ' + typeof fn);
+ }
+
+ this.function_ = fn;
+ this.arguments_ = args;
+ this.callback_ = callback;
+ this.results_ = [];
+
+ this.backoff_ = null;
+ this.strategy_ = null;
+ this.failAfter_ = -1;
+
+ this.state_ = FunctionCall.State_.PENDING;
+}
+util.inherits(FunctionCall, events.EventEmitter);
States in which the call can be.
+ +FunctionCall.State_ = {
Call isn't started yet.
+ + PENDING: 0,
Call is in progress.
+ + RUNNING: 1,
Call completed successfully which means that either the wrapped function +returned successfully or the maximal number of backoffs was reached.
+ + COMPLETED: 2,
The call was aborted.
+ + ABORTED: 3
+};
Checks whether the call is pending.
+ +FunctionCall.prototype.isPending = function() {
+ return this.state_ == FunctionCall.State_.PENDING;
+};
Checks whether the call is in progress.
+ +FunctionCall.prototype.isRunning = function() {
+ return this.state_ == FunctionCall.State_.RUNNING;
+};
Checks whether the call is completed.
+ +FunctionCall.prototype.isCompleted = function() {
+ return this.state_ == FunctionCall.State_.COMPLETED;
+};
Checks whether the call is aborted.
+ +FunctionCall.prototype.isAborted = function() {
+ return this.state_ == FunctionCall.State_.ABORTED;
+};
Sets the backoff strategy to use. Can only be called before the call is +started otherwise an exception will be thrown.
+ +FunctionCall.prototype.setStrategy = function(strategy) {
+ if (!this.isPending()) {
+ throw new Error('FunctionCall in progress.');
+ }
+ this.strategy_ = strategy;
+ return this; // Return this for chaining.
+};
Returns all intermediary results returned by the wrapped function since +the initial call.
+ +FunctionCall.prototype.getResults = function() {
+ return this.results_.concat();
+};
Sets the backoff limit.
+ +FunctionCall.prototype.failAfter = function(maxNumberOfRetry) {
+ if (!this.isPending()) {
+ throw new Error('FunctionCall in progress.');
+ }
+ this.failAfter_ = maxNumberOfRetry;
+ return this; // Return this for chaining.
+};
Aborts the call.
+ +FunctionCall.prototype.abort = function() {
+ if (this.isCompleted()) {
+ throw new Error('FunctionCall already completed.');
+ }
+
+ if (this.isRunning()) {
+ this.backoff_.reset();
+ }
+
+ this.state_ = FunctionCall.State_.ABORTED;
+};
Initiates the call to the wrapped function. Accepts an optional factory +function used to create the backoff instance; used when testing.
+ +FunctionCall.prototype.start = function(backoffFactory) {
+ if (this.isAborted()) {
+ throw new Error('FunctionCall aborted.');
+ } else if (!this.isPending()) {
+ throw new Error('FunctionCall already started.');
+ }
+
+ var strategy = this.strategy_ || new FibonacciBackoffStrategy();
+
+ this.backoff_ = backoffFactory ?
+ backoffFactory(strategy) :
+ new Backoff(strategy);
+
+ this.backoff_.on('ready', this.doCall_.bind(this));
+ this.backoff_.on('fail', this.doCallback_.bind(this));
+ this.backoff_.on('backoff', this.handleBackoff_.bind(this));
+
+ if (this.failAfter_ > 0) {
+ this.backoff_.failAfter(this.failAfter_);
+ }
+
+ this.state_ = FunctionCall.State_.RUNNING;
+ this.doCall_();
+};
Calls the wrapped function.
+ +FunctionCall.prototype.doCall_ = function() {
+ var eventArgs = ['call'].concat(this.arguments_);
+ events.EventEmitter.prototype.emit.apply(this, eventArgs);
+ var callback = this.handleFunctionCallback_.bind(this);
+ this.function_.apply(null, this.arguments_.concat(callback));
+};
Calls the wrapped function's callback with the last result returned by the +wrapped function.
+ +FunctionCall.prototype.doCallback_ = function() {
+ var args = this.results_[this.results_.length - 1];
+ this.callback_.apply(null, args);
+};
Handles wrapped function's completion. This method acts as a replacement +for the original callback function.
+ +FunctionCall.prototype.handleFunctionCallback_ = function() {
+ if (this.isAborted()) {
+ return;
+ }
+
+ var args = Array.prototype.slice.call(arguments);
+ this.results_.push(args); // Save callback arguments.
+ events.EventEmitter.prototype.emit.apply(this, ['callback'].concat(args));
+
+ if (args[0]) {
+ this.backoff_.backoff(args[0]);
+ } else {
+ this.state_ = FunctionCall.State_.COMPLETED;
+ this.doCallback_();
+ }
+};
Handles the backoff event by reemitting it.
+ +FunctionCall.prototype.handleBackoff_ = function(number, delay, err) {
+ this.emit('backoff', number, delay, err);
+};
+
+module.exports = FunctionCall;
Copyright (c) 2012 Mathieu Turcotte
+ Licensed under the MIT license.
+
+ var Backoff = require('./lib/backoff');
+var ExponentialBackoffStrategy = require('./lib/strategy/exponential');
+var FibonacciBackoffStrategy = require('./lib/strategy/fibonacci');
+var FunctionCall = require('./lib/function_call.js');
+
+module.exports.Backoff = Backoff;
+module.exports.FunctionCall = FunctionCall;
+module.exports.FibonacciStrategy = FibonacciBackoffStrategy;
+module.exports.ExponentialStrategy = ExponentialBackoffStrategy;
Constructs a Fibonacci backoff.
+ +module.exports.fibonacci = function(options) {
+ return new Backoff(new FibonacciBackoffStrategy(options));
+};
Constructs an exponential backoff.
+ +module.exports.exponential = function(options) {
+ return new Backoff(new ExponentialBackoffStrategy(options));
+};
Constructs a FunctionCall for the given function and arguments.
+ +module.exports.call = function(fn, vargs, callback) {
+ var args = Array.prototype.slice.call(arguments);
+ fn = args[0];
+ vargs = args.slice(1, args.length - 1);
+ callback = args[args.length - 1];
+ return new FunctionCall(fn, vargs, callback);
+};
Copyright (c) 2012 Mathieu Turcotte
+ Licensed under the MIT license.
+
+ var events = require('events');
+var util = require('util');
+
+function isDef(value) {
+ return value !== undefined && value !== null;
+}
Abstract class defining the skeleton for the backoff strategies. Accepts an +object holding the options for the backoff strategy:
+randomisationFactor
: The randomisation factor which must be between 0
+ and 1 where 1 equates to a randomization factor of 100% and 0 to no
+ randomization.initialDelay
: The backoff initial delay in milliseconds.maxDelay
: The backoff maximal delay in milliseconds.function BackoffStrategy(options) {
+ options = options || {};
+
+ if (isDef(options.initialDelay) && options.initialDelay < 1) {
+ throw new Error('The initial timeout must be greater than 0.');
+ } else if (isDef(options.maxDelay) && options.maxDelay < 1) {
+ throw new Error('The maximal timeout must be greater than 0.');
+ }
+
+ this.initialDelay_ = options.initialDelay || 100;
+ this.maxDelay_ = options.maxDelay || 10000;
+
+ if (this.maxDelay_ <= this.initialDelay_) {
+ throw new Error('The maximal backoff delay must be ' +
+ 'greater than the initial backoff delay.');
+ }
+
+ if (isDef(options.randomisationFactor) &&
+ (options.randomisationFactor < 0 || options.randomisationFactor > 1)) {
+ throw new Error('The randomisation factor must be between 0 and 1.');
+ }
+
+ this.randomisationFactor_ = options.randomisationFactor || 0;
+}
Gets the maximal backoff delay.
+ +BackoffStrategy.prototype.getMaxDelay = function() {
+ return this.maxDelay_;
+};
Gets the initial backoff delay.
+ +BackoffStrategy.prototype.getInitialDelay = function() {
+ return this.initialDelay_;
+};
Template method that computes and returns the next backoff delay in +milliseconds.
+ +BackoffStrategy.prototype.next = function() {
+ var backoffDelay = this.next_();
+ var randomisationMultiple = 1 + Math.random() * this.randomisationFactor_;
+ var randomizedDelay = Math.round(backoffDelay * randomisationMultiple);
+ return randomizedDelay;
+};
Computes and returns the next backoff delay. Intended to be overridden by +subclasses.
+ +BackoffStrategy.prototype.next_ = function() {
+ throw new Error('BackoffStrategy.next_() unimplemented.');
+};
Template method that resets the backoff delay to its initial value.
+ +BackoffStrategy.prototype.reset = function() {
+ this.reset_();
+};
Resets the backoff delay to its initial value. Intended to be overridden by +subclasses.
+ +BackoffStrategy.prototype.reset_ = function() {
+ throw new Error('BackoffStrategy.reset_() unimplemented.');
+};
+
+module.exports = BackoffStrategy;