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