summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBojan Djurkovic <dbojan@gmail.com>2016-08-02 12:55:26 -0300
committerBojan Djurkovic <dbojan@gmail.com>2016-08-02 12:55:26 -0300
commitcbc5b4f864f621fe6068a6455faca2c06418c90f (patch)
tree3dd07a8adffc7093be8668c0826ccc59aed66480
parent60dc3b658e4c6bdc8a2469e278a474e1586fa74a (diff)
downloadasync-cbc5b4f864f621fe6068a6455faca2c06418c90f.tar.gz
add filter option to retry() and retryable() to allow for error filtering and control of retry flow. Resolves #1256.
-rw-r--r--lib/retry.js31
-rw-r--r--mocha_test/retry.js102
-rw-r--r--mocha_test/retryable.js24
3 files changed, 154 insertions, 3 deletions
diff --git a/lib/retry.js b/lib/retry.js
index 8163ad9..a700516 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 4d7b241..1cfbcab 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 7147269..0a229f5 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;