Skip to content

Commit

Permalink
add filter option to retry() and retryable() to allow for error filte…
Browse files Browse the repository at this point in the history
…ring and control of retry flow. Resolves caolan#1256.
  • Loading branch information
bojand authored and hargasinski committed Oct 7, 2016
1 parent 3398782 commit 997e3bf
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 3 deletions.
31 changes: 29 additions & 2 deletions lib/retry.js
Expand Up @@ -19,6 +19,10 @@ 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).
* * `filter` - Synchronous function that is invoked on erroneous result with the
* the error. If it returns `true` the retry attempts will continue, if the
* function returns `false` the retry flow is aborting with the current
* attempt's error and result being returned to the final callback.
* * 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 +66,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({
* filter: 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 +84,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 +102,17 @@ export default function retry(opts, task, callback) {
acc.intervalFunc = typeof t.interval === 'function' ?
t.interval :
constant(+t.interval || DEFAULT_INTERVAL);

if(typeof t.filter === 'function') {
acc.filter = t.filter;
}
} 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 @@ -111,7 +129,16 @@ export default function retry(opts, task, callback) {
function retryAttempt() {
task(function(err) {
if (err && attempt++ < options.times) {
setTimeout(retryAttempt, options.intervalFunc(attempt));
var proceed = true;
if(options.filter) {
proceed = options.filter(err);
}

if(proceed) {
setTimeout(retryAttempt, options.intervalFunc(attempt));
} else {
callback.apply(null, arguments);
}
} 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 filter 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 filter(err) {
return err && err !== special;
}
var options = {
times: times,
filter: filter
};
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 filter 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 filter(err) {
return err && err === error + callCount; // just a different pattern
}
var options = {
filter: filter
};
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 filter 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 filter(err) {
return err && err !== special;
}
var start = new Date().getTime();
async.retry({ interval: interval, filter: filter }, 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 filter should not be called',function(done) {
var callCount = 0;
var error = 'ERROR';
var erroredResult = 'RESULT';
var filterCalled = false;
function fn(callback) {
callCount++;
callback(null, erroredResult + callCount);
}
function filter(err) {
filterCalled = true;
return err && err === error;
}
var options = {
filter: filter
};
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(filterCalled, false, "filter function was called");
done();
}));
});
});
24 changes: 24 additions & 0 deletions mocha_test/retryable.js
Expand Up @@ -21,6 +21,30 @@ describe('retryable', function () {
}, 15);
});

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

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

setTimeout(function () {
}, 15);
});

it('should work as an embedded task', function(done) {
var retryResult = 'RETRY';
var fooResults;
Expand Down

0 comments on commit 997e3bf

Please sign in to comment.