26

I want to perform an operation repeatedly, with an increasing timeout between each operation, until it succeeds or a certain amount of time elapses. How do I structure this with promises in Q?

metacubed
  • 7,031
  • 6
  • 36
  • 65
Jay Bienvenu
  • 3,069
  • 5
  • 33
  • 44
  • If you want a recursive snippet with native promises and max retries, check this: https://stackoverflow.com/a/44577075/3032209 – Yair Kukielka Jun 15 '17 at 21:14

6 Answers6

26

All the answers here are really complicated in my opinion. Kos has the right idea but you can shorten the code by writing more idiomatic promise code:

function retry(operation, delay) {
    return operation().catch(function(reason) {
        return Q.delay(delay).then(retry.bind(null, operation, delay * 2));
    });
}

And with comments:

function retry(operation, delay) {
    return operation(). // run the operation
        catch(function(reason) { // if it fails
            return Q.delay(delay). // delay 
               // retry with more time
               then(retry.bind(null, operation, delay * 2)); 
        });
}

If you want to time it out after a certain time (let's say 10 seconds , you can simply do:

var promise = retry(operation, 1000).timeout(10000);

That functionality is built right into Q, no need to reinvent it :)

João Pimentel Ferreira
  • 14,289
  • 10
  • 80
  • 109
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • 1
    This is a really great suggestion. However, I found an issue with it. Even if the timeout expires, the retry function will remain calling itself until it succeeds, which obviously could be infinitely. Is there a clean way to break the promise chain when the timeout occurs so that no more handlers are invoked, or are we stuck with having to set a flag, or even abandon timeout() in favour of a nrAttempts counter incremented on each call to operation() and checked against some limit? – JHH Mar 18 '15 at 20:19
  • @JHH if you let me use bluebird I'd use cancellation - otherwise you'd use a special closure variable to simulate it - or pass a "token" (variable) into the retries and set it to false. So there are ways but no really clean way with Q. – Benjamin Gruenbaum Mar 19 '15 at 22:52
  • 1
    Of course there are ways. I solved it very easily with a maxAttempts and a counter, and skipped using timeout altogether. My concern is more with the fact that the answer above is flawed in this respect, and if not careful people could end up spawning infinite operations that consume significant cpu over time. – JHH Mar 20 '15 at 14:35
  • Honestly - the timeout is fine - the issue is using exceptions for flow control to begin with - it'd make more sense to not use exceptions for flow control but rather return a result - and if that result is not successful then kill the operation - that way a timeout would let you easily raise an exception from the inside the get the behaviour you're expecting. That said - feel free to post an alternative answer. – Benjamin Gruenbaum Mar 20 '15 at 14:37
  • Kos' solution worked for me. But this solution didn't :-/ . Not sure why. I passed the function in the same way... – Jon49 Dec 15 '15 at 23:59
  • 1
    @JoãoPimentelFerreira Q is a library. You can replace `Q.delay(N)` with `new Promise(r => setTimeout(r, N))` and live happily ever after :) – Benjamin Gruenbaum Oct 28 '18 at 12:04
  • @BenjaminGruenbaum, thanks, I removed my question because then I saw the tag `q` on the OP question, but thanks for replying so quickly. – João Pimentel Ferreira Oct 28 '18 at 12:08
  • @BenjaminGruenbaum, nice compact function. Nonetheless I can't use this approach because my function `operation()` as such throws an error, even before calling retry, since the loading of my `function operation()` is deferred. Is there any workaround? – João Pimentel Ferreira Oct 28 '18 at 14:11
  • A promise returning function should _never_ throw an error. That said, you can use `Q.try` around it (or `Promise.resolve().then(operation)` in the native promises case). – Benjamin Gruenbaum Oct 28 '18 at 14:13
  • @BenjaminGruenbaum thanks again. I know it's beyond promises, but I got my solution: https://stackoverflow.com/a/53032624/1243247 – João Pimentel Ferreira Oct 28 '18 at 15:18
4

Here's an example of how I'd approach this, with some helper functions. Note, the 'maxTimeout' is the more complicated part because you have to race two states.

// Helper delay function to wait a specific amount of time.
function delay(time){
    return new Promise(function(resolve){
        setTimeout(resolve, time);
    });
}

// A function to just keep retrying forever.
function runFunctionWithRetries(func, initialTimeout, increment){
    return func().catch(function(err){
        return delay(initialTimeout).then(function(){
            return runFunctionWithRetries(
                    func, initialTimeout + increment, increment);
        });
    });
}

// Helper to retry a function, with incrementing and a max timeout.
function runFunctionWithRetriesAndMaxTimeout(
        func, initialTimeout, increment, maxTimeout){

    var overallTimeout = delay(maxTimeout).then(function(){
        // Reset the function so that it will succeed and no 
        // longer keep retrying.
        func = function(){ return Promise.resolve() };
        throw new Error('Function hit the maximum timeout');
    });

    // Keep trying to execute 'func' forever.
    var operation = runFunctionWithRetries(function(){
        return func();
    }, initialTimeout, increment);

    // Wait for either the retries to succeed, or the timeout to be hit.
    return Promise.race([operation, overallTimeout]);
}

Then to use these helpers, you'd do something like this:

// Your function that creates a promise for your task.
function doSomething(){
    return new Promise(...);
}

runFunctionWithRetriesAndMaxTimeout(function(){
    return doSomething();
}, 1000 /* start at 1s per try */, 500 /* inc by .5s */, 30000 /* max 30s */);
loganfsmyth
  • 156,129
  • 30
  • 331
  • 251
2

I think you can't do it on promise level - a promise isn't an operation, but is just a value that's going to arrive in the future, so you can't define a function typed Promise -> Promise that will achieve it.

You'd need to go one level down and define a function typed Operation -> Promise where Operation is itself typed () -> Promise. I assume the operation doesn't take any parameters - you can partially-apply them beforehand.

Here's a recursive approach that doubles the timeout on every run:

function RepeatUntilSuccess(operation, timeout) {
    var deferred = Q.defer();
    operation().then(function success(value) {
        deferred.resolve(value);
    }, function error(reason) {
        Q.delay(timeout
        .then(function() {
            return RepeatUntilSuccess(operation, timeout*2);
        }).done(function(value) {
            deferred.resolve(value);
        });
    });
    return deferred.promise;
}

Demo: http://jsfiddle.net/0dmzt53p/

Kos
  • 70,399
  • 25
  • 169
  • 233
  • 2
    Why the deferred? You can use promise chaining. – Benjamin Gruenbaum Nov 02 '14 at 00:32
  • Cool, thanks for showing me! Still learning these, I always end up not being straightforward enough. (Haven't yet got to trying them out outside fiddles) – Kos Nov 02 '14 at 00:45
  • Well, I [wrote a Q&A about this specific issue](http://stackoverflow.com/questions/23803743/what-is-the-deferred-antipattern-and-how-do-i-avoid-it) based on Petka's (link there). – Benjamin Gruenbaum Nov 02 '14 at 00:48
0

I did the following with Promises/A+ (which should be fine with Q)

function delayAsync(timeMs)
{
    return new Promise(function(resolve){
        setTimeout(resolve, timeMs);
    });
}

//use an IIFE so we can have a private scope
//to capture some state    
(function(){
    var a;
    var interval = 1000;
    a = function(){
        return doSomethingAsync()
            .then(function(success){
                if(success)
                {
                    return true;
                }
                return delayAsync(interval)
                         .then(function(){
                             interval *= 2;
                         })
                         .then(a());
            });
    };
    a();
})();

I'm sure you could figure out how to bail after a maximum timeout.

spender
  • 117,338
  • 33
  • 229
  • 351
0
  1. Assign a boolean variable for "all process timeout".
  2. Call window's setTimeout to make that variable 'false' after that "all process timeout".
  3. Call promise operation with a timeout.
  4. If it succeeds no problem.
  5. If it fails: In promise's error handler, call promise function again with an increased timeout if the boolean variable is true.

Something like this:

var goOn= true;

setTimeout(function () {
    goOn= false;
}, 30000); // 30 seconds -- all process timeout


var timeout = 1000; // 1 second

(function () {
    var helperFunction = function () {

        callAsyncFunc().then(function () {
            // success...
        }, function () {
            // fail
            if (goOn) {
                timeout += 1000; // increase timeout 1 second
                helperFunction();
            }
        }).timeout(timeout);

    }
})();
Kursad Gulseven
  • 1,978
  • 1
  • 24
  • 26
0

My sugesstion would be to use the bluebird-retry library

To install

npm i bluebird-retry

 var Promise = require('bluebird');
 var retry = require('bluebird-retry');
 var count = 0;
function myfunc() {
    console.log('myfunc called ' + (++count) + ' times '+new Date().toISOString());
    return Promise.reject(new Error(''));
}
retry(myfunc,{ max_tries: 5, interval: 5000, backoff: 2 })
    .then(function(result) {
        console.log(result);
    });

The above program tries the promise flow 5 times with interval * backoff backoff interval between every retry.

Also, should you require to pass any arguments, pass it as args which accepts array of arguments. Include it in options section where max_retries, interval and backoff is mentioned.

Here is the official documentation https://www.npmjs.com/package/bluebird-retry

Merv
  • 83
  • 1
  • 12