diff --git a/lib/retry.js b/lib/retry.js index 8163ad94b..a70051641 100644 --- a/lib/retry.js +++ b/lib/retry.js @@ -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 @@ -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({ @@ -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; @@ -87,6 +102,10 @@ 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 { @@ -94,7 +113,6 @@ export default function retry(opts, task, callback) { } } - if (arguments.length < 3 && typeof opts === 'function') { callback = task || noop; task = opts; @@ -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); } diff --git a/mocha_test/retry.js b/mocha_test/retry.js index 4d7b241cb..1cfbcab78 100644 --- a/mocha_test/retry.js +++ b/mocha_test/retry.js @@ -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(); + })); + }); }); diff --git a/mocha_test/retryable.js b/mocha_test/retryable.js index 7147269da..0a229f50d 100644 --- a/mocha_test/retryable.js +++ b/mocha_test/retryable.js @@ -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;