Skip to content

Commit

Permalink
Support custom non-retryable errors
Browse files Browse the repository at this point in the history
  • Loading branch information
fiznool committed Sep 15, 2016
1 parent 86fdd0f commit 730c9fe
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 22 deletions.
51 changes: 36 additions & 15 deletions lib/backoff.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

var Promise = require('bluebird');
var _ = require('lodash');
var Errors = require('./errors');

function Backoff(options) {
Expand All @@ -10,13 +11,16 @@ function Backoff(options) {
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() {
Expand All @@ -33,19 +37,36 @@ 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, cls => 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.
Expand All @@ -60,22 +81,22 @@ BackoffRunner.NonRetryableError = Errors.NonRetryableError;
* return cb(null, body);
* }
* }
*
*
* var options = {
* minDelay: 100,
* 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
Expand All @@ -97,7 +118,7 @@ BackoffRunner.attempt = function(operation, options, done) {
operation(function() {
if(arguments.length > 0 && arguments[0]) {
// The operation ended with an error.
if(arguments[0] instanceof BackoffRunner.NonRetryableError) {
if(backoff.isNonRetryableError(arguments[0])) {
// We shouldn't retry.
return done(arguments[0]);
}
Expand All @@ -121,11 +142,11 @@ BackoffRunner.attempt = function(operation, options, done) {

/**
* 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:
Expand All @@ -139,16 +160,16 @@ BackoffRunner.attempt = function(operation, options, done) {
* resolve(body);
* }
* });
*
*
* var options = {
* minDelay: 100,
* 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
Expand All @@ -169,7 +190,7 @@ BackoffRunner.attemptAsync = function(operation, options) {
return Promise.resolve()
.then(operation)
.catch(function(err) {
if(err instanceof BackoffRunner.NonRetryableError) {
if(backoff.isNonRetryableError(err)) {
// We shouldn't retry.
throw err;
}
Expand Down
119 changes: 112 additions & 7 deletions spec/specs.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

var Utils = require('../lib/index.js'),
var util = require('util'),
Utils = require('../lib/index.js'),
_ = require('lodash');

describe('Utils', function() {
Expand Down Expand Up @@ -1442,9 +1443,9 @@ describe('Utils', function() {
});

describe('Backoff', function() {

describe('attempt', function() {

it('should perform the operation once if it succeeds', function(done) {
var attempts = 0;
var work = function(done) {
Expand Down Expand Up @@ -1532,7 +1533,7 @@ describe('Utils', function() {
done();
});
});

it('should fail the operation if the maximum duration is reached', function(done) {
var attempts = 0;
var work = function(done) {
Expand Down Expand Up @@ -1573,10 +1574,60 @@ describe('Utils', function() {
done();
});
});

it('should fail the operation if a custom NonRetryableError occurs', function(done) {
function NonRetryableError() {}
util.inherits(NonRetryableError, Error);
var attempts = 0;
var nrtErr = new NonRetryableError('ERROR');
var work = function(done) {
attempts++;
done(nrtErr);
};
var options = {
minDelay: 10,
maxDelay: 20,
maxAttempts: 3,
nonRetryableErrors: NonRetryableError
};
Utils.Backoff.attempt(work, options, function(err) {
expect(err).toBe(nrtErr);
expect(attempts).toBe(1);
done();
});
});

it('should fail the operation if one of multiple custom NonRetryableErrors occurs', function(done) {
function NonRetryableError1() {}
util.inherits(NonRetryableError1, Error);
function NonRetryableError2() {}
util.inherits(NonRetryableError2, Error);

var attempts = 0;
var nrtErr = new NonRetryableError1('ERROR');
var work = function(done) {
attempts++;
done(nrtErr);
};
var options = {
minDelay: 10,
maxDelay: 20,
maxAttempts: 3,
nonRetryableErrors: [
NonRetryableError1,
NonRetryableError2
]
};
Utils.Backoff.attempt(work, options, function(err) {
expect(err).toBe(nrtErr);
expect(attempts).toBe(1);
done();
});
});
});

describe('attemptAsync', function() {

it('should perform the operation once if it succeeds', function(done) {
var attempts = 0;
var work = function() {
Expand Down Expand Up @@ -1673,7 +1724,7 @@ describe('Utils', function() {
done();
});
});

it('should fail the operation if the maximum duration is reached', function(done) {
var attempts = 0;
var work = function() {
Expand Down Expand Up @@ -1718,6 +1769,60 @@ describe('Utils', function() {
done();
});
});

it('should fail the operation if a custom NonRetryableError occurs', function(done) {
function NonRetryableError() {}
util.inherits(NonRetryableError, Error);
var attempts = 0;
var nrtErr = new NonRetryableError('ERROR');
var work = function() {
attempts++;
throw nrtErr;
};
var options = {
minDelay: 10,
maxDelay: 20,
maxAttempts: 3,
nonRetryableErrors: NonRetryableError
};
Utils.Backoff
.attemptAsync(work, options)
.catch(function(err) {
expect(err).toBe(nrtErr);
expect(attempts).toBe(1);
done();
});
});

it('should fail the operation if one of multiple custom NonRetryableErrors occurs', function(done) {
function NonRetryableError1() {}
util.inherits(NonRetryableError1, Error);
function NonRetryableError2() {}
util.inherits(NonRetryableError2, Error);

var attempts = 0;
var nrtErr = new NonRetryableError1('ERROR');
var work = function() {
attempts++;
throw nrtErr;
};
var options = {
minDelay: 10,
maxDelay: 20,
maxAttempts: 3,
nonRetryableErrors: [
NonRetryableError1,
NonRetryableError2
]
};
Utils.Backoff
.attemptAsync(work, options)
.catch(function(err) {
expect(err).toBe(nrtErr);
expect(attempts).toBe(1);
done();
});
});
});
});

Expand Down

0 comments on commit 730c9fe

Please sign in to comment.