191 lines
6.0 KiB
JavaScript
191 lines
6.0 KiB
JavaScript
|
// Copyright (c) 2012 Mathieu Turcotte
|
||
|
// Licensed under the MIT license.
|
||
|
|
||
|
var events = require('events');
|
||
|
var precond = require('precond');
|
||
|
var util = require('util');
|
||
|
|
||
|
var Backoff = require('./backoff');
|
||
|
var FibonacciBackoffStrategy = require('./strategy/fibonacci');
|
||
|
|
||
|
// Wraps a function to be called in a backoff loop.
|
||
|
function FunctionCall(fn, args, callback) {
|
||
|
events.EventEmitter.call(this);
|
||
|
|
||
|
precond.checkIsFunction(fn, 'Expected fn to be a function.');
|
||
|
precond.checkIsArray(args, 'Expected args to be an array.');
|
||
|
precond.checkIsFunction(callback, 'Expected callback to be a function.');
|
||
|
|
||
|
this.function_ = fn;
|
||
|
this.arguments_ = args;
|
||
|
this.callback_ = callback;
|
||
|
this.lastResult_ = [];
|
||
|
this.numRetries_ = 0;
|
||
|
|
||
|
this.backoff_ = null;
|
||
|
this.strategy_ = null;
|
||
|
this.failAfter_ = -1;
|
||
|
this.retryPredicate_ = FunctionCall.DEFAULT_RETRY_PREDICATE_;
|
||
|
|
||
|
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
|
||
|
};
|
||
|
|
||
|
// The default retry predicate which considers any error as retriable.
|
||
|
FunctionCall.DEFAULT_RETRY_PREDICATE_ = function(err) {
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
// 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) {
|
||
|
precond.checkState(this.isPending(), 'FunctionCall in progress.');
|
||
|
this.strategy_ = strategy;
|
||
|
return this; // Return this for chaining.
|
||
|
};
|
||
|
|
||
|
// Sets the predicate which will be used to determine whether the errors
|
||
|
// returned from the wrapped function should be retried or not, e.g. a
|
||
|
// network error would be retriable while a type error would stop the
|
||
|
// function call.
|
||
|
FunctionCall.prototype.retryIf = function(retryPredicate) {
|
||
|
precond.checkState(this.isPending(), 'FunctionCall in progress.');
|
||
|
this.retryPredicate_ = retryPredicate;
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
// Returns all intermediary results returned by the wrapped function since
|
||
|
// the initial call.
|
||
|
FunctionCall.prototype.getLastResult = function() {
|
||
|
return this.lastResult_.concat();
|
||
|
};
|
||
|
|
||
|
// Returns the number of times the wrapped function call was retried.
|
||
|
FunctionCall.prototype.getNumRetries = function() {
|
||
|
return this.numRetries_;
|
||
|
};
|
||
|
|
||
|
// Sets the backoff limit.
|
||
|
FunctionCall.prototype.failAfter = function(maxNumberOfRetry) {
|
||
|
precond.checkState(this.isPending(), 'FunctionCall in progress.');
|
||
|
this.failAfter_ = maxNumberOfRetry;
|
||
|
return this; // Return this for chaining.
|
||
|
};
|
||
|
|
||
|
// Aborts the call.
|
||
|
FunctionCall.prototype.abort = function() {
|
||
|
if (this.isCompleted() || this.isAborted()) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (this.isRunning()) {
|
||
|
this.backoff_.reset();
|
||
|
}
|
||
|
|
||
|
this.state_ = FunctionCall.State_.ABORTED;
|
||
|
this.lastResult_ = [new Error('Backoff aborted.')];
|
||
|
this.emit('abort');
|
||
|
this.doCallback_();
|
||
|
};
|
||
|
|
||
|
// 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) {
|
||
|
precond.checkState(!this.isAborted(), 'FunctionCall is aborted.');
|
||
|
precond.checkState(this.isPending(), '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, true /* isRetry */));
|
||
|
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_(false /* isRetry */);
|
||
|
};
|
||
|
|
||
|
// Calls the wrapped function.
|
||
|
FunctionCall.prototype.doCall_ = function(isRetry) {
|
||
|
if (isRetry) {
|
||
|
this.numRetries_++;
|
||
|
}
|
||
|
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() {
|
||
|
this.callback_.apply(null, this.lastResult_);
|
||
|
};
|
||
|
|
||
|
// 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.lastResult_ = args; // Save last callback arguments.
|
||
|
events.EventEmitter.prototype.emit.apply(this, ['callback'].concat(args));
|
||
|
|
||
|
var err = args[0];
|
||
|
if (err && this.retryPredicate_(err)) {
|
||
|
this.backoff_.backoff(err);
|
||
|
} 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;
|