summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCaolan McMahon <caolan.mcmahon@gmail.com>2014-03-30 13:03:26 +0100
committerCaolan McMahon <caolan.mcmahon@gmail.com>2014-03-30 13:03:26 +0100
commit7f427ba1dd8707419c501169a3193a8b5a0126ae (patch)
treed56dfff65fd5e170f2f939019ead24fb0a714165
parentb7144bbc0322593b4d285a802dc6ef67f92f4d6a (diff)
parentdf06f5d166c4cf8930d9db2afd8410fadb1bd241 (diff)
downloadasync-7f427ba1dd8707419c501169a3193a8b5a0126ae.tar.gz
Merge pull request #475 from jessehouchins/async.retry
Async.retry
-rw-r--r--README.md42
-rwxr-xr-xlib/async.js31
-rwxr-xr-xtest/test-async.js56
3 files changed, 129 insertions, 0 deletions
diff --git a/README.md b/README.md
index 0a87149..9c018fa 100644
--- a/README.md
+++ b/README.md
@@ -146,6 +146,7 @@ Usage:
* [`queue`](#queue)
* [`cargo`](#cargo)
* [`auto`](#auto)
+* [`retry`](#retry)
* [`iterator`](#iterator)
* [`apply`](#apply)
* [`nextTick`](#nextTick)
@@ -1328,6 +1329,47 @@ new tasks much easier (and the code more readable).
---------------------------------------
+<a name="retry" />
+### retry([times = 5], task, [callback])
+
+Attempts to get a successful response from `task` no more than `times` times before
+returning an error. If the task is successful, the `callback` will be passed the result
+of the successfull task. If all attemps fail, the callback will be passed the error and
+result (if any) of the final attempt.
+
+__Arguments__
+
+* `task(callback, results)` - A function which receives two arguments: (1) a `callback(err, result)`
+ which must be called when finished, passing `err` (which can be `null`) and the `result` of
+ the function's execution, and (2) a `results` object, containing the results of
+ the previously executed functions (if nested inside another control flow).
+* `callback(err, results)` - An optional callback which is called when the
+ task has succeeded, or after the final failed attempt. It receives the `err` and `result` arguments of the last attempt at completing the `task`.
+
+The [`retry`](#retry) function can be used as a stand-alone control flow by passing a
+callback, as shown below:
+
+```js
+async.retry(3, apiMethod, function(err, result) {
+ // do something with the result
+});
+```
+
+It can also be embeded within other control flow functions to retry individual methods
+that are not as reliable, like this:
+
+```js
+async.auto({
+ users: api.getUsers.bind(api),
+ payments: async.retry(3, api.getPayments.bind(api))
+}, function(err, results) {
+ // do something with the results
+});
+```
+
+
+---------------------------------------
+
<a name="iterator" />
### iterator(tasks)
diff --git a/lib/async.js b/lib/async.js
index e5e90fa..4d460b3 100755
--- a/lib/async.js
+++ b/lib/async.js
@@ -481,6 +481,37 @@
});
};
+ async.retry = function(times, task, callback) {
+ var DEFAULT_TIMES = 5;
+ var attempts = [];
+ // Use defaults if times not passed
+ if (typeof times === 'function') {
+ callback = task;
+ task = times;
+ times = DEFAULT_TIMES;
+ }
+ // Make sure times is a number
+ times = parseInt(times, 10) || DEFAULT_TIMES;
+ var wrappedTask = function(wrappedCallback, wrappedResults) {
+ var retryAttempt = function(task, finalAttempt) {
+ return function(seriesCallback) {
+ task(function(err, result){
+ seriesCallback(!err || finalAttempt, {err: err, result: result});
+ }, wrappedResults);
+ };
+ };
+ while (times) {
+ attempts.push(retryAttempt(task, !(times-=1)));
+ }
+ async.series(attempts, function(done, data){
+ data = data[data.length - 1];
+ (wrappedCallback || callback)(data.err, data.result);
+ });
+ }
+ // If a callback is passed, run this as a controll flow
+ return callback ? wrappedTask() : wrappedTask
+ };
+
async.waterfall = function (tasks, callback) {
callback = callback || function () {};
if (!_isArray(tasks)) {
diff --git a/test/test-async.js b/test/test-async.js
index d93180d..e7a8820 100755
--- a/test/test-async.js
+++ b/test/test-async.js
@@ -579,6 +579,62 @@ exports['auto modifying results causes final callback to run early'] = function(
});
};
+// Issue 306 on github: https://github.com/caolan/async/issues/306
+exports['retry when attempt succeeds'] = function(test) {
+ var failed = 3
+ var callCount = 0
+ var expectedResult = 'success'
+ function fn(callback, results) {
+ callCount++
+ failed--
+ if (!failed) callback(null, expectedResult)
+ else callback(true) // respond with error
+ }
+ async.retry(fn, function(err, result){
+ test.equal(callCount, 3, 'did not retry the correct number of times')
+ test.equal(result, expectedResult, 'did not return the expected result')
+ test.done();
+ });
+};
+
+exports['retry when all attempts succeeds'] = function(test) {
+ var times = 3;
+ var callCount = 0;
+ var error = 'ERROR';
+ var erroredResult = 'RESULT';
+ function fn(callback, results) {
+ callCount++;
+ callback(error + callCount, erroredResult + callCount); // respond with indexed values
+ };
+ async.retry(times, fn, function(err, result){
+ test.equal(callCount, 3, "did not retry the correct number of times");
+ test.equal(err, error + times, "Incorrect error was returned");
+ test.equal(result, erroredResult + times, "Incorrect result was returned");
+ test.done();
+ });
+};
+
+exports['retry as an embedded task'] = function(test) {
+ var retryResult = 'RETRY';
+ var fooResults;
+ var retryResults;
+
+ async.auto({
+ foo: function(callback, results){
+ fooResults = results;
+ callback(null, 'FOO');
+ },
+ retry: async.retry(function(callback, results) {
+ retryResults = results;
+ callback(null, retryResult);
+ })
+ }, function(err, results){
+ test.equal(results.retry, retryResult, "Incorrect result was returned from retry function");
+ test.equal(fooResults, retryResults, "Incorrect results were passed to retry function");
+ test.done();
+ });
+};
+
exports['waterfall'] = function(test){
test.expect(6);
var call_order = [];