web-dev-qa-db-fra.com

Promettre de réessayer les modèles de conception

Modifier

  1. Modèle qui réessaie jusqu'à la résolution de la promesse (avec délai et MaxRetries). 
  2. Motif qui réessaie jusqu'à ce que la condition Se rencontre sur le résultat (avec délai et MaxRetries).
  3. Un modèle dynamique efficace en mémoire avec des tentatives illimitées (délai fourni).

Code pour # 1. Continue à réessayer jusqu'à ce que promesse soit résolue (toute communauté d'améliorations pour la langue, etc.?)

Promise.retry = function(fn, times, delay) {
    return new Promise(function(resolve, reject){
        var error;
        var attempt = function() {
            if (times == 0) {
                reject(error);
            } else {
                fn().then(resolve)
                    .catch(function(e){
                        times--;
                        error = e;
                        setTimeout(function(){attempt()}, delay);
                    });
            }
        };
        attempt();
    });
};

Utilisation

work.getStatus()
    .then(function(result){ //retry, some glitch in the system
        return Promise.retry(work.unpublish.bind(work, result), 10, 2000);
    })
    .then(function(){console.log('done')})
    .catch(console.error);

Code pour # 2 continuez de réessayer jusqu'à ce qu'une condition se rencontre sur le résultat then de manière réutilisable (la condition est ce qui variera).

work.publish()
    .then(function(result){
        return new Promise(function(resolve, reject){
            var intervalId = setInterval(function(){
                work.requestStatus(result).then(function(result2){
                    switch(result2.status) {
                        case "progress": break; //do nothing
                        case "success": clearInterval(intervalId); resolve(result2); break;
                        case "failure": clearInterval(intervalId); reject(result2); break;
                    }
                }).catch(function(error){clearInterval(intervalId); reject(error)});
            }, 1000);
        });
    })
    .then(function(){console.log('done')})
    .catch(console.error);
32
user2727195

Quelque chose d'un peu différent ...

Les tentatives asynchrones peuvent être réalisées en créant une chaîne .catch(), par opposition à la chaîne plus habituelle .then().

Cette approche est:

  • possible uniquement avec un nombre maximal de tentatives spécifié. (La chaîne doit être de longueur finie),
  • conseillé seulement avec un faible maximum. (Les chaînes de promesses consomment de la mémoire à peu près proportionnellement à leur longueur).

Sinon, utilisez une solution récursive.

Tout d'abord, une fonction utilitaire à utiliser comme rappel .catch().

var t = 500;

function rejectDelay(reason) {
    return new Promise(function(resolve, reject) {
        setTimeout(reject.bind(null, reason), t); 
    });
}

Maintenant, vous pouvez construire des chaînes .catch de manière très concise:

1. Réessayer jusqu'à ce que la promesse soit résolue, avec délai

var max = 5;
var p = Promise.reject();

for(var i=0; i<max; i++) {
    p = p.catch(attempt).catch(rejectDelay);
}
p = p.then(processResult).catch(errorHandler);

DEMO: https://jsfiddle.net/duL0qjqe/

2. Réessayer jusqu'à ce que le résultat réponde à une condition, sans délai

var max = 5;
var p = Promise.reject();

for(var i=0; i<max; i++) {
    p = p.catch(attempt).then(test);
}
p = p.then(processResult).catch(errorHandler);

DEMO: https://jsfiddle.net/duL0qjqe/1/

3. Réessayer jusqu'à ce que le résultat réponde à une condition, avec délai

Après avoir réfléchi aux questions (1) et (2), un test combiné + délai est également trivial.

var max = 5;
var p = Promise.reject();

for(var i=0; i<max; i++) {
    p = p.catch(attempt).then(test).catch(rejectDelay);
    // Don't be tempted to simplify this to `p.catch(attempt).then(test, rejectDelay)`. Test failures would not be caught.
}
p = p.then(processResult).catch(errorHandler);

test() peut être synchrone ou asynchrone.

Il serait également trivial d'ajouter des tests supplémentaires. Il suffit de mettre en sandwich une chaîne de thens entre les deux prises.

p = p.catch(attempt).then(test1).then(test2).then(test3).catch(rejectDelay);

DEMO: https://jsfiddle.net/duL0qjqe/3/


Toutes les versions sont conçues pour que attempt soit une fonction asynchrone à retour de promesse. Il pourrait aussi éventuellement renvoyer une valeur, auquel cas la chaîne suivrait son chemin de réussite jusqu'au prochain/terminal .then().

30
Roamer-1888

2. Modèle qui réessaie jusqu'à ce que la condition remplie le résultat (avec délai et maxRetries)

C'est une bonne manière de faire cela avec des promesses autochtones de manière récursive:

const wait = ms => new Promise(r => setTimeout(r, ms));

const retryOperation = (operation, delay, times) => new Promise((resolve, reject) => {
  return operation()
    .then(resolve)
    .catch((reason) => {
      if (times - 1 > 0) {
        return wait(delay)
          .then(retryOperation.bind(null, operation, delay, times - 1))
          .then(resolve)
          .catch(reject);
      }
      return reject(reason);
    });
});

Voici comment vous l'appelez, en supposant que func réussit parfois et échoue parfois, en renvoyant toujours une chaîne que nous pouvons consigner:

retryOperation(func, 1000, 5)
  .then(console.log)
  .catch(console.log);

Ici, nous appelons retryOperation pour lui demander de réessayer toutes les secondes et avec un nombre maximal de tentatives = 5.

Si vous voulez quelque chose de plus simple sans promesses, RxJs pourrait vous aider: https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/retrywhen.md

14
Yair Kukielka

Vous pouvez chaîner une nouvelle promesse sur la précédente, retardant ainsi sa résolution éventuelle jusqu'à ce que vous connaissiez la réponse finale. Si la réponse suivante n'est toujours pas connue, enchaînez une autre promesse et continuez à enchaîner checkStatus () jusqu'à ce que vous connaissiez la réponse et puissiez renvoyer la résolution finale. Cela pourrait fonctionner comme ceci:

function delay(t) {
    return new Promise(function(resolve) {
        setTimeout(resolve, t);
    });
}

function checkStatus() {
    return work.requestStatus().then(function(result) {
        switch(result.status) {
            case "success":
                return result;      // resolve
            case "failure":
                throw result;       // reject
            case default:
            case "inProgress": //check every second
                return delay(1000).then(checkStatus);
        }
    });
}

work.create()
    .then(work.publish) //remote work submission
    .then(checkStatus)
    .then(function(){console.log("work published"})
    .catch(console.error);

Remarque, j'ai également évité de créer la promesse autour de votre déclaration switch. Puisque vous êtes déjà dans un gestionnaire .then(), le simple renvoi d'une valeur est une résolution, le lancement d'une exception est un rejet et le renvoi d'une promesse consiste à chaîner une nouvelle promesse sur la précédente. Cela couvre les trois branches de votre déclaration switch sans créer de nouvelle promesse. Pour plus de commodité, j'utilise une fonction delay() basée sur des promesses.

Pour votre information, cela suppose que work.requestStatus() n'a besoin d'aucun argument. Si des arguments spécifiques sont nécessaires, vous pouvez les transmettre au moment de l’appel de la fonction.


Il peut également être judicieux d’implémenter une sorte de valeur de délai d’attente pendant la durée de la boucle d’attente, de sorte que cela ne dure jamais éternellement. Vous pouvez ajouter la fonctionnalité de délai d'attente comme ceci:

function delay(t) {
    return new Promise(function(resolve) {
        setTimeout(resolve, t);
    });
}

function checkStatus(timeout) {
    var start = Date.now();

    function check() {
        var now = Date.now();
        if (now - start > timeout) {
            return Promise.reject(new Error("checkStatus() timeout"));
        }
        return work.requestStatus().then(function(result) {
            switch(result.status) {
                case "success":
                    return result;      // resolve
                case "failure":
                    throw result;       // reject
                case default:
                case "inProgress": //check every second
                    return delay(1000).then(check);
            }
        });
    }
    return check;
}

work.create()
    .then(work.publish) //remote work submission
    .then(checkStatus(120 * 1000))
    .then(function(){console.log("work published"})
    .catch(console.error);

Je ne sais pas exactement quel "modèle de conception" vous recherchez. Puisque vous semblez vous opposer à la fonction déclarée en externe checkStatus(), voici une version intégrée:

work.create()
    .then(work.publish) //remote work submission
    .then(work.requestStatus)
    .then(function() {
        // retry until done
        var timeout = 10 * 1000;
        var start = Date.now();

        function check() {
            var now = Date.now();
            if (now - start > timeout) {
                return Promise.reject(new Error("checkStatus() timeout"));
            }
            return work.requestStatus().then(function(result) {
                switch(result.status) {
                    case "success":
                        return result;      // resolve
                    case "failure":
                        throw result;       // reject
                    case default:
                    case "inProgress": //check every second
                        return delay(1000).then(check);
                }
            });
        }
        return check();
    }).then(function(){console.log("work published"})
    .catch(console.error);

Un schéma de réessai plus réutilisable qui pourrait être utilisé dans de nombreuses circonstances définirait un code externe réutilisable, mais vous semblez vous opposer à cela et je n'ai donc pas créé cette version.


Voici une autre approche qui utilise une méthode .retryUntil() sur le Promise.prototype selon votre demande. Si vous souhaitez modifier les détails de mise en œuvre de cette opération, vous devriez pouvoir modifier cette approche générale:

// fn returns a promise that must be fulfilled with an object
//    with a .status property that is "success" if done.  Any
//    other value for that status means to continue retrying
//  Rejecting the returned promise means to abort processing 
//        and propagate the rejection
// delay is the number of ms to delay before trying again
//     no delay before the first call to the callback
// tries is the max number of times to call the callback before rejecting
Promise.prototype.retryUntil = function(fn, delay, tries) {
    var numTries = 0;
    function check() {
        if (numTries >= tries) {
            throw new Error("retryUntil exceeded max tries");
        }
        ++numTries;
        return fn().then(function(result) {
            if (result.status === "success") {
                return result;          // resolve
            } else {
                return Promise.delay(delay).then(check);
            }
        });
    }
    return this.then(check);
}

if (!Promise.delay) {
    Promise.delay = function(t) {
        return new Promise(function(resolve) {
            setTimeout(resolve, t);
        });
    }
}


work.create()
    .then(work.publish) //remote work submission
    .retryUntil(function() {
        return work.requestStatus().then(function(result) {
            // make this promise reject for failure
            if (result.status === "failure") {
                throw result;
            }
            return result;
        })
    }, 2000, 10).then(function() {
        console.log("work published");
    }).catch(console.error);

Je ne peux toujours pas vraiment dire ce que vous voulez ou ce que toutes ces approches ne résout pas. Étant donné que vos approches semblent toutes être du code en ligne et ne pas utiliser d’aide fiable, voici l’une de celles-ci:

work.create()
    .then(work.publish) //remote work submission
    .then(function() {
        var tries = 0, maxTries = 20;
        function next() {
            if (tries > maxTries) {
                throw new Error("Too many retries in work.requestStatus");
            }
            ++tries;
            return work.requestStatus().then(function(result) {
                switch(result.status) {
                    case "success":
                        return result;
                    case "failure":
                        // if it failed, make this promise reject
                        throw result;
                    default:
                        // for anything else, try again after short delay
                        // chain to the previous promise
                        return Promise.delay(2000).then(next);
                }

            });
        }
        return next();
    }).then(function(){
        console.log("work published")
    }).catch(console.error);
8
jfriend00

Il y a beaucoup de bonnes solutions mentionnées et maintenant, avec async/wait, ces problèmes peuvent être résolus sans effort.

Si une approche récursive ne vous dérange pas, voici ma solution.

function retry(fn, retries=3, err=null) {
  if (!retries) {
    return Promise.reject(err);
  }
  return fn().catch(err => {
      return retry(fn, (retries - 1), err);
    });
}
6
holmberd

Une bibliothèque peut le faire facilement: promise-retry .

Voici quelques exemples pour le tester:

const promiseRetry = require('promise-retry');

Attendez-vous à ce que la deuxième tentative réussisse:

it('should retry one time after error', (done) => {
    const options = {
        minTimeout: 10,
        maxTimeout: 100
    };
    promiseRetry((retry, number) => {
        console.log('test2 attempt number', number);
        return new Promise((resolve, reject) => {
            if (number === 1) throw new Error('first attempt fails');
            else resolve('second attempt success');
        }).catch(retry);
    }, options).then(res => {
        expect(res).toBe('second attempt success');
        done();
    }).catch(err => {
        fail(err);
    });
});

Attendez-vous à une nouvelle tentative:

it('should not retry a second time', (done) => {
    const options = {
        retries: 1,
        minTimeout: 10,
        maxTimeout: 100
    };
    promiseRetry((retry, number) => {
        console.log('test4 attempt number', number);
        return new Promise((resolve, reject) => {
            if (number <= 2) throw new Error('attempt ' + number + ' fails');
            else resolve('third attempt success');
        }).catch(retry);
    }, options).then(res => {
        fail('Should never success');
    }).catch(err => {
        expect(err.toString()).toBe('Error: attempt 2 fails');
        done();
    });
});
0
Bludwarf
work.create()
    .then(work.publish) //remote work submission
    .then(function(result){
        var maxAttempts = 10;
        var handleResult = function(result){
            if(result.status === 'success'){
                return result;
            }
            else if(maxAttempts <= 0 || result.status === 'failure') {
                return Promise.reject(result);
            }
            else {
                maxAttempts -= 1;
                return (new Promise( function(resolve) {
                    setTimeout( function() {
                        resolve(_result);
                    }, 1000);
                })).then(function(){
                    return work.requestStatus().then(handleResult);
                });
            }
        };
        return work.requestStatus().then(handleResult);
    })
    .then(function(){console.log("work published"})
    .catch(console.error);
0
Hugo Silva