115 votes

Promesse Modèles de conception de réessai

Modifier

  1. Modèle qui continue de réessayer jusqu'à ce que la promesse se résolve (avec delay et maxRetries).
  2. Modèle qui continue de réessayer jusqu'à ce que la condition soit respectée dans le résultat (avec delay et maxRetries).
  3. Un modèle dynamique économique en mémoire avec des tentatives illimitées (avec delay fourni).

Code pour #1. Continue de réessayer jusqu'à ce que la promesse se résolve (toute amélioration de la communauté pour le langage, 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 continuer de réessayer jusqu'à ce qu'une condition soit remplie dans le résultat de then de manière réutilisable (la condition 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);

0 votes

Pas sûr de ce que le setInterval accomplira à l'intérieur de la promesse, où est-il résolu ?

0 votes

@jfriend, qu'est-il arrivé à la réponse, pourquoi a-t-elle été supprimée?

1 votes

Ne "edits" n'ajoutez pas à votre question. Cela rend difficile à suivre. Au lieu de cela, modifiez simplement votre question. Si quelqu'un veut consulter l'historique des modifications, alors il le peut.

92voto

Roamer-1888 Points 1442

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

Les tentatives asynchrones peuvent être réalisées en construisant une chaîne de .catch(), plutôt que la chaîne plus habituelle de .then().

Cette approche est :

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

Autrement, utilisez une solution récursive.

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

var t = 500;

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

Vous pouvez maintenant construire des chaînes de .catch très succinctes :

1. Réessayez jusqu'à ce que la promesse se résolve, avec un délai

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

for(var i=0; i

**DÉMO**: [https://jsfiddle.net/duL0qjqe/](https://jsfiddle.net/duL0qjqe/)

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

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

    for(var i=0; i

DÉMO: https://jsfiddle.net/duL0qjqe/1/

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

Après avoir compris (1) et (2), un test+retard combinés sont tout aussi triviaux.

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

for(var i=0; i

``

test() peut être synchrone ou asynchrone.

Il serait également trivial d'ajouter d'autres tests. Il suffit d'insérer une chaîne de .then() entre les deux .catch().

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

DÉMO: https://jsfiddle.net/duL0qjqe/3/


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

`` ``` ````

1 votes

Lorsque vous dites, seulement possible avec un nombre maximum d'essais spécifié, je peux revenir à la méthode basée sur setInterval pour illimitée et effectuer la concaténation de promesses dans la méthode setInterval, ce serait intéressant de voir votre exemple pour les essais illimités, si cela ne vous dérange pas. Il sera utilisé lorsque je suis sûr du résultat, que ce soit resolve ou reject, mais cette procédure (publication) prend parfois trop de temps en fonction de la taille du fichier.

0 votes

Les chaînes de promesses consomment de la mémoire approximativement proportionnelle à leur longueur mais vont être libérées à la fin du règlement ?

0 votes

J'ai édité #3 en tant que tentatives illimitées, et je m'attends à une concaténation/chaînage dynamique et efficace en termes de mémoire.

55voto

Yair Kukielka Points 81

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

C'est une façon agréable de le faire avec des promesses natives de façon récursive :

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

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

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

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

Ici, nous appelons retryOperation en lui demandant de réessayer toutes les secondes et avec un maximum de 5 tentatives.

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

0 votes

J'aime cette approche mais il y a un bug lors de la vérification des temps restants pour réessayer. Il devrait être si (times > 0) {...

0 votes

Je suppose que cela dépend de la façon dont vous interprétez le nombre de "fois" que vous voulez réessayer une opération. Mais si vous prenez en compte que l'opération a déjà été exécutée une fois avant le catch, c'est correct.

0 votes

Je m'attendrais à ce que le troisième paramètre soit temps ou tentatives. Essayez d'exécuter retryOperation(func, 1000, 1) - la fonction ne sera pas exécutée (réessayée) en cas d'échec. Aurait-il du sens d'appeler retryOperation(func, 1000, 0) et de s'attendre à ce que l'opération ne soit pas du tout appelée ? - Le paramètre "0" devrait signifier : aucun réessai - pas 0 appels de l'opération.

43voto

holmberd Points 429

Il y a beaucoup de bonnes solutions mentionnées et maintenant avec async/await, ces problèmes peuvent être résolus sans trop d'efforts.

Si vous n'avez rien contre une approche récursive, 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);
    });
}

1 votes

J'aime cette approche simple qui résiste à l'épreuve du temps. Je rajouterais simplement un gestionnaire d'erreurs.

1 votes

@DavidLemon oui, il est très probable que vous auriez un catch() sur votre retry() pour vous fournir la dernière erreur des tentatives et l'information que toutes les tentatives ont échoué.

0 votes

Cela a été très utile. Je voulais exécuter fn jusqu'à obtention d'une sortie souhaitée, ou jusqu'à ce que le nombre de tentatives soit épuisé, donc j'ai ajouté un .then() avant le .catch(). Dans mon cas d'utilisation, le .then() fait l'appel récursif si nécessaire, et le .catch() rejette les erreurs.

18voto

jfriend00 Points 152127

Vous pouvez enchaîner une nouvelle promesse sur la précédente, retardant ainsi sa résolution finale jusqu'à ce que vous connaissiez la réponse finale. Si la réponse suivante n'est toujours pas connue, enchaînez une autre promesse dessus et continuez à enchaîner checkStatus() sur elle-même jusqu'à ce que vous connaissiez finalement 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;      // résoudre
            case "failure":
                throw result;       // rejeter
            case default:
            case "inProgress": //vérifier chaque seconde
                return delay(1000).then(checkStatus);
        }
    });
}

work.create()
    .then(work.publish) //soumission du travail à distance
    .then(checkStatus)
    .then(function(){console.log("travail publié"})
    .catch(console.error);

À noter, j'ai également évité de créer la promesse autour de votre instruction switch. Étant donné que vous êtes déjà dans un gestionnaire .then(), simplement retourner une valeur est une résolution, jeter une exception est un rejet et retourner une promesse consiste à enchaîner une nouvelle promesse sur la précédente. Cela couvre les trois branches de votre instruction switch sans créer de nouvelle promesse à l'intérieur. Pour plus de commodité, j'utilise une fonction delay() basée sur une promesse.

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


Il pourrait également être utile de mettre en place une sorte de valeur de temporisation pour savoir combien de temps vous allez boucler en attendant la fin, pour que cela ne dure jamais éternellement. Vous pourriez ajouter la fonctionnalité de temporisation de la manière suivante :

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

function checkStatus(temporisation) {
    var début = Date.now();

    function vérifier() {
        var maintenant = Date.now();
        if (maintenant - début > temporisation) {
            return Promise.reject(new Error("Délai dépassé pour checkStatus()"));
        }
        return work.requestStatus().then(function(result) {
            switch(result.status) {
                case "success":
                    return result;      // résoudre
                case "failure":
                    throw result;       // rejeter
                case default:
                case "inProgress": //vérifier chaque seconde
                    return delay(1000).then(verify);
            }
        });
    }
    return vérifier;
}

work.create()
    .then(work.publish) //soumission du travail à distance
    .then(checkStatus(120 * 1000))
    .then(function(){console.log("travail publié"})
    .catch(console.error);

Je ne suis pas sûr exactement du "patron de conception" que vous recherchez. Comme vous semblez vous opposer à la fonction checkStatus() déclarée de manière externe, voici une version en ligne :

work.create()
    .then(work.publish) //soumission du travail à distance
    .then(work.requestStatus)
    .then(function() {
        // réessayer jusqu'à la fin
        var temporisation = 10 * 1000;
        var début = Date.now();

        function vérifier() {
            var maintenant = Date.now();
            if (maintenant - début > temporisation) {
                return Promise.reject(new Error("Délai dépassé pour checkStatus()"));
            }
            return work.requestStatus().then(function(result) {
                switch(result.status) {
                    case "success":
                        return result;      // résoudre
                    case "failure":
                        throw result;       // rejeter
                    case default:
                    case "inProgress": //vérifier chaque seconde
                        return delay(1000).then(verify);
                }
            });
        }
        return check();
    }).then(function(){console.log("travail publié"})
    .catch(console.error);

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


Voici une autre approche qui utilise une méthode .retryUntil() sur le Promise.prototype comme vous l'avez demandé. Si vous voulez ajuster les détails de mise en œuvre de ceci, vous devriez pouvoir modifier cette approche générale :

// fn renvoie une promesse qui doit être remplie avec un objet
// ayant une propriété .status qui est "success" si terminé. Toute
// autre valeur pour ce statut signifie de continuer à réessayer
// Rejeter la promesse renvoyée signifie d'abandonner le traitement
// et de propager le rejet
// delay représente le nombre de ms avant de réessayer
// pas de délai avant le premier appel à la fonction de rappel
// tries est le nombre maximal de tentatives d'appel de la fonction de rappel avant de rejeter
Promise.prototype.retryUntil = function(fn, delay, tries) {
    var numTries = 0;
    function check() {
        if (numTries >= tries) {
            throw new Error("retryUntil a dépassé le nombre maximal d'essais");
        }
        ++numTries;
        return fn().then(function(result) {
            if (result.status === "success") {
                return result;          // résoudre
            } 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) //soumission du travail à distance
    .retryUntil(function() {
        return work.requestStatus().then(function(result) {
            // faire rejeter cette promesse pour un échec
            if (result.status === "failure") {
                throw result;
            }
            return result;
        })
    }, 2000, 10).then(function() {
        console.log("travail publié");
    }).catch(console.error);

Je ne sais toujours pas vraiment ce que vous voulez ou ce qui, dans toutes ces approches, ne résout pas votre problème. Comme vos approches semblent toutes être du code en ligne et ne pas utiliser d'aide réutilisable, en voici une :

work.create()
    .then(work.publish) //soumission du travail à distance
    .then(function() {
        var essais = 0, maxEssais = 20;
        function suivant() {
            if (essais > maxEssais) {
                throw new Error("Trop de réessais dans work.requestStatus");
            }
            ++essais;
            return work.requestStatus().then(function(result) {
                switch(result.status) {
                    case "success":
                        return result;
                    case "failure":
                        // s'il échoue, faire rejeter cette promesse
                        throw result;
                    default:
                        // pour tout le reste, réessayer après un court délai
                        // enchaîner à la promesse précédente
                        return Promise.delay(2000).then(suivant);
                }

            });
        }
        return suivant();
    }).then(function(){
        console.log("travail publié")
    }).catch(console.error);

0 votes

J'ai aimé l'implémentation précédente, je l'ai résolu aussi d'une certaine manière mais je n'aime pas ça, et de même pour votre cas, j'ai aimé votre implémentation précédente que vous avez supprimée, et je l'ai aimée à cause de son implémentation en ligne dans le gestionnaire then, le but est de trouver un modèle de conception élégant qui fonctionne avec le gestionnaire then sans avoir besoin de fonctions extérieures et plutôt que simplement résoudre le problème, pouvons-nous le retravailler s'il vous plaît

0 votes

@user2727195 - La précédente implémentation n'a pas fonctionné - c'est pourquoi je l'ai changée. Elle ne bouclait pas jusqu'à ce que le résultat soit trouvé. Elle appelait simplement work.requestStatus() une fois de plus et n'avait aucune capacité de bouclage. Je pense que cela répond aux exigences de votre question. Si vous cherchez quelque chose de plus, veuillez modifier votre question pour spécifier ce que vous recherchez d'autre et laissez-moi un commentaire pour me dire que vous l'avez fait.

1 votes

@user2727195 - L'implémentation checkStatus() peut être réalisée en ligne sans une fonction nommée séparément si cela vous dérange d'une manière ou d'une autre, mais j'ai pensé que c'était une implémentation plus propre de la diviser comme je l'ai fait parce que cela rend la chaîne .then().then().then() beaucoup plus simple à suivre et à voir exactement ce qui se passe. De plus, j'ai dû créer une fermeture pour la gestion du délai et c'était un moyen pratique de le faire sans introduire de variables au niveau de la portée supérieure.

0voto

Hugo Silva Points 868
work.create()
    .then(work.publish) // soumission du travail à distance
    .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("travail publié"})
    .catch(console.error);

0 votes

Évitez le schéma antipattern du constructeur de promesses!

0 votes

@Bergi - Je ne peux pas comprendre comment résoudre cela de manière plus lisible. Pourriez-vous s'il vous plaît éclairer ma lanterne ? De plus, pourriez-vous faire défiler jusqu'au bas de cet article (github.com/petkaantonov/bluebird/wiki/…), où il parle de setTimeout, et vous assurer que ce scénario ne rentre pas dans l'exception ?

0 votes

Jetez un œil à la réponse de jfriend :-) setTimeout ne correspond pas à l'exception, utilisez donc le constructeur Promise pour obtenir une promesse pour le délai, mais requestStatus renvoie une promesse et l'utiliser à l'intérieur du constructeur Promise est un anti-pattern.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X