Skip to content

Commit

Permalink
Merge pull request #1261 from bojand/master
Browse files Browse the repository at this point in the history
add filter option to retry() and retryable(). PR for #1256.
  • Loading branch information
megawac committed Aug 8, 2016
2 parents 8ea5ea8 + 4367751 commit ff65da5
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 5 deletions.
23 changes: 21 additions & 2 deletions lib/retry.js
Expand Up @@ -19,6 +19,11 @@ import constant from 'lodash/constant';
* * `interval` - The time to wait between retries, in milliseconds. The
* default is `0`. The interval may also be specified as a function of the
* retry count (see example).
* * `errorFilter` - An optional synchronous function that is invoked on
* erroneous result. If it returns `true` the retry attempts will continue;
* if the function returns `false` the retry flow is aborted with the current
* attempt's error and result being returned to the final callback.
* Invoked with (err).
* * If `opts` is a number, the number specifies the number of times to retry,
* with the default interval of `0`.
* @param {Function} task - A function which receives two arguments: (1) a
Expand Down Expand Up @@ -62,6 +67,16 @@ import constant from 'lodash/constant';
* // do something with the result
* });
*
* // try calling apiMethod only when error condition satisfies, all other
* // errors will abort the retry control flow and return to final callback
* async.retry({
* errorFilter: function(err) {
* return err.message === 'Temporary error'; // only retry on a specific error
* }
* }, apiMethod, function(err, result) {
* // do something with the result
* });
*
* // It can also be embedded within other control flow functions to retry
* // individual methods that are not as reliable, like this:
* async.auto({
Expand All @@ -70,6 +85,7 @@ import constant from 'lodash/constant';
* }, function(err, results) {
* // do something with the results
* });
*
*/
export default function retry(opts, task, callback) {
var DEFAULT_TIMES = 5;
Expand All @@ -87,14 +103,15 @@ export default function retry(opts, task, callback) {
acc.intervalFunc = typeof t.interval === 'function' ?
t.interval :
constant(+t.interval || DEFAULT_INTERVAL);

acc.errorFilter = t.errorFilter;
} else if (typeof t === 'number' || typeof t === 'string') {
acc.times = +t || DEFAULT_TIMES;
} else {
throw new Error("Invalid arguments for async.retry");
}
}


if (arguments.length < 3 && typeof opts === 'function') {
callback = task || noop;
task = opts;
Expand All @@ -110,7 +127,9 @@ export default function retry(opts, task, callback) {
var attempt = 1;
function retryAttempt() {
task(function(err) {
if (err && attempt++ < options.times) {
if (err && attempt++ < options.times &&
(typeof options.errorFilter != 'function' ||
options.errorFilter(err))) {
setTimeout(retryAttempt, options.intervalFunc(attempt));
} else {
callback.apply(null, arguments);
Expand Down
102 changes: 101 additions & 1 deletion mocha_test/retry.js
Expand Up @@ -156,5 +156,105 @@ describe("retry", function () {
async.retry(5, fn, function(err, result) {
expect(result).to.be.eql({a: 1});
});
})
});

it('retry when all attempts fail and error continue test returns true',function(done) {
var times = 3;
var callCount = 0;
var error = 'ERROR';
var special = 'SPECIAL_ERROR';
var erroredResult = 'RESULT';
function fn(callback) {
callCount++;
callback(error + callCount, erroredResult + callCount);
}
function errorTest(err) {
return err && err !== special;
}
var options = {
times: times,
errorFilter: errorTest
};
async.retry(options, fn, function(err, result){
assert.equal(callCount, 3, "did not retry the correct number of times");
assert.equal(err, error + times, "Incorrect error was returned");
assert.equal(result, erroredResult + times, "Incorrect result was returned");
done();
});
});

it('retry when some attempts fail and error test returns false at some invokation',function(done) {
var callCount = 0;
var error = 'ERROR';
var special = 'SPECIAL_ERROR';
var erroredResult = 'RESULT';
function fn(callback) {
callCount++;
var err = callCount === 2 ? special : error + callCount;
callback(err, erroredResult + callCount);
}
function errorTest(err) {
return err && err === error + callCount; // just a different pattern
}
var options = {
errorFilter: errorTest
};
async.retry(options, fn, function(err, result){
assert.equal(callCount, 2, "did not retry the correct number of times");
assert.equal(err, special, "Incorrect error was returned");
assert.equal(result, erroredResult + 2, "Incorrect result was returned");
done();
});
});

it('retry with interval when some attempts fail and error test returns false at some invokation',function(done) {
var interval = 50;
var callCount = 0;
var error = 'ERROR';
var erroredResult = 'RESULT';
var special = 'SPECIAL_ERROR';
var specialCount = 3;
function fn(callback) {
callCount++;
var err = callCount === specialCount ? special : error + callCount;
callback(err, erroredResult + callCount);
}
function errorTest(err) {
return err && err !== special;
}
var start = new Date().getTime();
async.retry({ interval: interval, errorFilter: errorTest }, fn, function(err, result){
var now = new Date().getTime();
var duration = now - start;
assert(duration >= (interval * (specialCount - 1)), 'did not include interval');
assert.equal(callCount, specialCount, "did not retry the correct number of times");
assert.equal(err, special, "Incorrect error was returned");
assert.equal(result, erroredResult + specialCount, "Incorrect result was returned");
done();
});
});

it('retry when first attempt succeeds and error test should not be called',function(done) {
var callCount = 0;
var error = 'ERROR';
var erroredResult = 'RESULT';
var continueTestCalled = false;
function fn(callback) {
callCount++;
callback(null, erroredResult + callCount);
}
function errorTest(err) {
continueTestCalled = true;
return err && err === error;
}
var options = {
errorFilter: errorTest
};
async.retry(options, fn, _.rest(function(args) {
assert.equal(callCount, 1, "did not retry the correct number of times");
expect(args).to.be.eql([null, erroredResult + callCount]);
assert.equal(continueTestCalled, false, "error test function was called");
done();
}));
});
});
22 changes: 20 additions & 2 deletions mocha_test/retryable.js
Expand Up @@ -16,9 +16,27 @@ describe('retryable', function () {
expect(calls).to.equal(3);
done();
});
});

it('basics with error test function', function (done) {
var calls = 0;
var special = 'special';
var opts = {
errorFilter: function(err) {
return err == special;
}
};
var retryableTask = async.retryable(opts, function (arg, cb) {
calls++;
expect(arg).to.equal(42);
cb(calls === 3 ? 'fail' : special);
});

setTimeout(function () {
}, 15);
retryableTask(42, function (err) {
expect(err).to.equal('fail');
expect(calls).to.equal(3);
done();
});
});

it('should work as an embedded task', function(done) {
Expand Down

0 comments on commit ff65da5

Please sign in to comment.