blob: 068b53a8ae1464613a5a458ac8cd3122bfeec083 [file]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
* A class for attempting some unreliable operation until it succeeds. Sort of
* an asynchronous while loop.
*
* (new mr.Retry(attempt, 500, 10)).start().then(onSuccess, onFailure);
*
* The next attempt is driven by the failure of current attempt. Thus, there is
* at most one attempt invocation at any time.
*/
goog.provide('mr.Retry');
goog.require('mr.Assertions');
goog.require('mr.PromiseResolver');
/**
* An object that attempts some operation until it succeeds.
*
* @template R
*/
mr.Retry = class {
/**
* @param {function():!Promise<R>} onAttempt An
* idempotent function to call repeatedly until it succeeds.
* @param {number} retryDelay The number of milliseconds to wait between the
* start of one attempt and the start of the next. It must be positive.
* More precisely, this is the amount of time to wait between the failure
* of one call to onAttempt().
* @param {number} maxAttempts The maximum number of attempts.
* It must be positive.
*/
constructor(onAttempt, retryDelay, maxAttempts) {
/**
* @private {!function():!Promise<R>}
*/
this.onAttempt_ = onAttempt;
/**
* @private {number}
*/
this.retryDelay_ = retryDelay > 0 ? retryDelay : 10;
/**
* @private {number}
*/
this.maxAttempts_ = maxAttempts > 0 ? maxAttempts : 1;
/**
* @private {number}
*/
this.maxRetryDelay_ = 0;
/**
* @private {number}
*/
this.backoffFactor_ = 1;
/**
* The number of times {@code onAttempt_} has been called.
* @private {number}
*/
this.numAttemptsStarted_ = 0;
/**
* @private {boolean}
*/
this.isFinished_ = false;
/**
* The ID of the most recently created timer.
* @private {?number}
*/
this.timerId_ = null;
/** @private {mr.PromiseResolver} */
this.resolver_ = null;
}
/**
* Starts running this object.
*
* This method starts an asynchronous process that repeatedly calls
* {@code onAttempt}.
*
* For each attempt, {@code onAttempt} is called. When {@code onAttempt}
* resolves, the returned promise is resolved with the same result.
* The returned promise rejects if if {@code abort} is called on this object,
* or the number of attempts specified by {@code setMaxAttempts} is reached.
*
* @return {!Promise<R>}
* @template R
*/
start() {
if (this.resolver_ != null) {
return Promise.reject(Error('Cannot call Retry.start more than once.'));
}
this.resolver_ = new mr.PromiseResolver();
this.retryOnce_();
return this.resolver_.promise;
}
/**
* Makes the next call to {@code onAttempt_}.
* @private
*/
retryOnce_() {
this.timerId_ = null;
if (this.isFinished_) {
// The abort method has been called, don't start a new attempt.
return;
}
this.numAttemptsStarted_++;
this.onAttempt_().then(
result => {
this.cleanup_();
this.resolver_.resolve(result);
},
error => {
if (this.numAttemptsStarted_ >= this.maxAttempts_) {
// Maximum number of attempts has been reached, do not try again.
this.cleanup_();
this.resolver_.reject(Error('Max attempts reached'));
} else {
this.timerId_ =
setTimeout(this.retryOnce_.bind(this), this.retryDelay_);
this.updateRetryDelay_();
}
});
}
/**
* Implement exponential backoff.
* @private
*/
updateRetryDelay_() {
let newRetryDelay = this.retryDelay_ * this.backoffFactor_;
if (this.maxRetryDelay_ > 0) {
newRetryDelay = Math.min(newRetryDelay, this.maxRetryDelay_);
}
this.retryDelay_ = newRetryDelay;
}
/**
* Sets the backoff factor. After each attempt, the retry delay is
* multiplied by the backoff factor, which must be at least 1.
*
* @param {number} backoffFactor The factor by which the retry delay should be
* increased after each attempt.
* @return {!mr.Retry} this
*/
setBackoffFactor(backoffFactor) {
mr.Assertions.assert(backoffFactor >= 1);
this.backoffFactor_ = backoffFactor;
return this;
}
/**
* Sets the maximum retry delay that can be used as a result of the backoff
* factor. If 0, there is no maximum.
* @param {number} maxRetryDelay The maximum number of milliseconds to wait
* before retrying.
* @return {!mr.Retry} this
*/
setMaxRetryDelay(maxRetryDelay) {
mr.Assertions.assert(maxRetryDelay >= 0);
this.maxRetryDelay_ = maxRetryDelay;
return this;
}
/**
* Causes this object to stop making attempts and puts it in a
* finished state. May be called any time after {@code start}.
*/
abort() {
this.cleanup_();
this.resolver_.reject(Error('abort'));
}
/**
* @private
*/
cleanup_() {
if (this.timerId_ != null) {
// Clean up any timer, because it will be a no-op when it fires.
clearTimeout(this.timerId_);
this.timerId_ = null;
}
this.isFinished_ = true;
}
};