flowxo-utils
Version:
Common utilities for Flow XO.
215 lines (189 loc) • 5.89 kB
JavaScript
var Promise = require('bluebird');
var _ = require('lodash');
var Errors = require('./errors');
function Backoff(options) {
this.attempts = 0;
this.minDelay = options.minDelay;
this.maxDelay = options.maxDelay;
this.useRandom = options.useRandom || false;
this.maxAttempts = options.maxAttempts;
this.setNonRetryableErrors(options.nonRetryableErrors);
if(options.hasOwnProperty('maxDuration')) {
this.maxDuration = options.maxDuration;
this.expiresAt = Date.now() + this.maxDuration;
this.hasExpired = function(afterDelay) {
return Date.now() + afterDelay > this.expiresAt;
};
}
}
Backoff.prototype.nextDelay = function() {
var random = (this.useRandom ? Math.random() : 0) + 1;
var delay = Math.round(random * this.minDelay * Math.pow(2, (this.attempts - 1)));
return Math.min(delay, this.maxDelay);
};
Backoff.prototype.hasExpired = function() {
return false;
};
Backoff.prototype.shouldRetry = function(afterDelay) {
return this.attempts < this.maxAttempts && !this.hasExpired(afterDelay);
};
Backoff.prototype.setNonRetryableErrors = function(errs) {
this.nonRetryableErrorClasses = [];
if(_.isArray(errs)) {
this.nonRetryableErrorClasses = errs;
} else if(errs) {
this.nonRetryableErrorClasses = [errs];
}
this.nonRetryableErrorClasses.push(Errors.NonRetryableError);
return this;
};
Backoff.prototype.isNonRetryableError = function(err) {
return _.some(this.nonRetryableErrorClasses, function(cls) {
return err instanceof cls;
});
};
var BackoffRunner = {};
BackoffRunner.NonRetryableError = Errors.NonRetryableError;
/**
* Attempts `operation` according to the `options`.
*
* The `done` node-style callback is called once the
* operation completes successfully, or the maximum
* number of attempts is reached. Each retry is
* performed according to the backoff rules passed in
* with the options.
*
* `operation` is passed a node-style 'error-first'
* callback when it is called. You should call this
* callback as appropriate in your calling code.
*
* Example:
*
* var operation = function(cb) {
* request(options, function(err, res, body) {
* if(err) {
* return cb(err);
* }
* return cb(null, body);
* }
* }
*
* var options = {
* minDelay: 100,
* maxDelay: 1000,
* maxAttempts: 5
* };
*
* Backoff.attempt(operation, options, function(err, result) {
* // `err` is populated if every attempt failed.
* // It will be populated with the last error
* // that occurred.
* // Otherwise, `result` is the result passed above.
* });
*
* `options` takes the following keys:
*
* - minDelay: the minimum delay to use for backoff
* - maxDelay: the maximum delay to use for backoff
* - maxAttempts: the maximum number of attempts before failing
* - maxDuration: the maximum duration that the backoff should take,
* in ms
* - useRandom: whether the backoff should be calcuated using random
* values or not
*
* @param {Function} operation work to carry out
* @param {Object} options options for retries
* @param {Function} done called when the work is finished
*/
BackoffRunner.attempt = function(operation, options, done) {
var backoff = new Backoff(options);
var performOperation = function() {
backoff.attempts++;
operation(function() {
if(arguments.length > 0 && arguments[0]) {
// The operation ended with an error.
if(backoff.isNonRetryableError(arguments[0])) {
// We shouldn't retry.
return done(arguments[0]);
}
var delay = backoff.nextDelay();
if(backoff.shouldRetry(delay)) {
// Try again after the delay.
return setTimeout(performOperation, delay);
}
// Else, fail.
return done(arguments[0]);
}
// Otherwise, the operation succeeded!
done.apply(null, arguments);
});
};
performOperation();
};
/**
* Attempts `operation` according to the `options`.
*
* Returns a promise which is resolved once the
* operation completes successfully, or rejected if
* the maximum number of attempts is reached.
*
* `operation` is expected to return a promise.
*
* Example:
*
* var operation = function() {
* return new Promise(function (resolve, reject) {
* request(options, function(err, res, body) {
* if(err) {
* return reject(err);
* }
* resolve(body);
* }
* });
*
* var options = {
* minDelay: 100,
* maxDelay: 1000,
* maxAttempts: 5
* };
* return backoff.attemptAsync(operation, options);
*
* `options` takes the following keys:
*
* - minDelay: the minimum delay to use for backoff
* - maxDelay: the maximum delay to use for backoff
* - maxAttempts: the maximum number of attempts before failing
* - maxDuration: the maximum duration that the backoff should take,
* in ms
* - useRandom: whether the backoff should be calcuated using random
* values or not
*
* @param {Function} operation work to carry out
* @param {Object} options options for retries
* @param {Function} done called when the work is finished
*/
BackoffRunner.attemptAsync = function(operation, options) {
var backoff = new Backoff(options);
function performOperation() {
backoff.attempts++;
return Promise.resolve()
.then(operation)
.catch(function(err) {
if(backoff.isNonRetryableError(err)) {
// We shouldn't retry.
throw err;
}
var delay = backoff.nextDelay();
if(backoff.shouldRetry(delay)) {
// Try again after the delay.
return Promise.delay(delay).then(performOperation);
}
// Otherwise, return the last error.
throw err;
});
}
return performOperation();
};
module.exports = BackoffRunner;
module.exports._Backoff = Backoff;
;