123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190 |
- // 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;
|