Utilisation de Polly pour les retraits (Français (French))

Utilisation de Polly pour les retraits

Comments

NOTE: Apart from English (and even then it's questionable, I'm Scottish). These are machine translated in languages I don't read. If they're terrible please contact me.
You can see how this translation was done in this article.

Sunday, 15 September 2024

//

9 minute read

Présentation

Polly est une partie critique de la boîte à outils du développeur.NET. Il s'agit d'une bibliothèque qui vous permet de définir des politiques pour le traitement des exceptions et des requêtes dans votre application. Dans cet article, nous explorerons comment Annexe I utiliser Polly pour gérer les relevés dans cette application.

Polly

Bien que Polly récupère vraiment bien ce n'est pas tout ce qu'il peut faire, c'est vraiment une boîte à outils pour ajouter de la résilience à vos applications. Les deux font des appels vers des services extérieurs et à l'interne.

Ceux-ci sont tirés de la Page principale de Polly et sont les principaux modèles que vous pouvez utiliser avec Polly:

  • Essayez à nouveau si quelque chose échoue. Cela peut être utile lorsque le problème est temporaire et pourrait disparaître.
  • Disjoncteur : Arrêtez d'essayer si quelque chose est cassé ou occupé. Cela peut vous être bénéfique en évitant de perdre du temps et d'aggraver les choses. Il peut également soutenir le système pour récupérer.
  • Abandonner si quelque chose prend trop de temps. Cela peut améliorer vos performances en libérant de l'espace et des ressources.
  • Limiteur de taux : Limitez le nombre de demandes que vous faites ou que vous acceptez. Cela peut vous permettre de contrôler la charge et de prévenir les problèmes ou les pénalités.
  • Fallback: Faites autre chose si quelque chose échoue. Cela peut améliorer votre expérience utilisateur et maintenir le programme en marche. Gudging: Faites plus d'une chose en même temps et prenez la plus rapide. Cela peut rendre votre programme plus rapide et plus réactif.

Comment utiliser Polly

Dans cette application, j'utilise Polly en plusieurs endroits.

ContexteService

Pour démarrer mon service de traduction et vérifier les serveurs EasyNMT sont disponibles. Thsi me permet de vérifier que le service est disponible avant de commencer à 'offrir' le service de traduction dans mon application. Vous vous souviendrez que c'est utilisé pour mon éditeur 'toy' pour vous permettre de traduire le balisage et pour mon 'à la volée' blog poste moteur de traductionC'est ce que j'ai dit. Donc c'est critique je vérifie que EasyNMT n'est pas descendu (et permet d'attendre jusqu'à ce qu'il arrive ; ce qui peut prendre quelques secondes).

 private async Task StartupHealthCheck(CancellationToken cancellationToken)
    {
        var retryPolicy = Policy
            .HandleResult<bool>(result => !result) // Retry when Ping returns false (service not available)
            .WaitAndRetryAsync(3, // Retry 3 times
                attempt => TimeSpan.FromSeconds(5), // Wait 5 seconds between retries
                (result, timeSpan, retryCount, context) =>
                {
                    logger.LogWarning("Translation service is not available, retrying attempt {RetryCount}", retryCount);
                });

        try
        {
            var isUp = await retryPolicy.ExecuteAsync(async () =>
            {
                return await Ping(cancellationToken); // Ping to check if the service is up
            });

            if (isUp)
            {
                logger.LogInformation("Translation service is available");
                TranslationServiceUp = true;
            }
            else
            {
                logger.LogError("Translation service is not available after retries");
                await HandleTranslationServiceFailure();
                TranslationServiceUp = false;
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "An error occurred while checking the translation service availability");
            await HandleTranslationServiceFailure();
            TranslationServiceUp = false;
        }
    }

Ici vous pouvez voir que nous avons mis en place une politique Polly Retry qui réessayera 3 fois avec une attente de 5 secondes entre chaque réessayer. Si le service n'est toujours pas disponible après les relevés, nous enregistrons une erreur et traitons l'échec en réglant le TranslationServiceUp Drapeau à faux. Cela permet ensuite à tous les services utilisant le service de traduction de savoir qu'il n'est pas disponible.

graph LR A[Start Health Check] --> B[Define Retry Policy] B --> C[Retry Policy: Retry 3 times] C --> D[Wait 5 seconds between retries] D --> E[Ping Translation Service] E --> F{Ping successful?} F -- Yes --> G[Log: Translation service is available] G --> H[Set TranslationServiceUp = true] F -- No --> I[Log: Translation service not available] I --> J[Check retry count] J -- Retry Limit Reached --> K[Log: Translation service not available after retries] K --> L[HandleTranslationServiceFailure] L --> M[Set TranslationServiceUp = false] J -- Retry Again --> E E --> N{Exception Occurs?} N -- Yes --> O[Log: Error occurred] O --> L

Umami.Net

J'utilise également Polly dans ma bibliothèque Umami.Net pour gérer les requêtes lors de la présentation des requêtes à l'API Umami. Il s'agit d'une partie critique de la bibliothèque car elle me permet de gérer tous les problèmes avec l'API et de réessayer la requête si nécessaire.

Ici, j'ai mis en place mon HttpClient d'utiliser une politique de réessayer; dans ce cas, je vérifie HttpStatusCode.ServiceUnavailable et de réessayer la demande si elle se produit. J'utilise aussi un Decorrelated Jitter Backoff stratégie d'attente entre les rétries. Il s'agit d'une bonne stratégie à utiliser car elle permet d'éviter le problème du « troupeau dénudé » où tous les clients réessayent en même temps (même si ce n'est que moi :)). Cela peut aider à réduire la charge sur le serveur et améliorer les chances de succès de la requête.

     var httpClientBuilder = services.AddHttpClient<AuthService>(options =>
            {
                options.BaseAddress = new Uri(umamiSettings.UmamiPath);
            })
            .SetHandlerLifetime(TimeSpan.FromMinutes(5)) 
            .AddPolicyHandler(RetryPolicyExtension.GetRetryPolicy());


    public static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
    {
        var delay = Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromSeconds(1), 3);
        return HttpPolicyExtensions
            .HandleTransientHttpError()
            .OrResult(msg => msg.StatusCode == HttpStatusCode.ServiceUnavailable)
            .WaitAndRetryAsync(delay);
    }

Utilisation d'une politique de ré-essai pour HttpClient Les demandes sont un moyen important d'améliorer la fiabilité. Alors que nous aimons penser que nos services web sont toujours disponibles il ya toujours SOME temps d'arrêt (dans mon cas quand par exemple La Tour de Garde détecte une mise à jour est en attente et redémarre le conteneur Umami). Ainsi, avoir une politique de réessayer en place peut aider à s'assurer que votre application peut gérer ces situations gracieusement.

FileSystemWatcher

Une autre utilisation que je fais de Polly est quand il s'agit de charger et d'enregistrer des fichiers dans mon application. J'utilise un FileSystemWatcher pour surveiller le répertoire où mes fichiers de balisage sont stockés. Quand un fichier est créé ou mis à jour, je charge le fichier et le traite. Cela peut être un problème si le fichier est toujours écrit à quand l'événement est déclenché. Donc j'utilise un RetryPolicy pour gérer cette situation.

Ici vous pouvez voir que je gère le IOException qui est lancé lorsque le fichier est en cours d'utilisation et réessayer l'opération. J'utilise un WaitAndRetryAsync politique de réessayer l'opération 5 fois avec un retard entre chaque réessayer. Cela me permet de gérer la situation où le fichier est encore écrit et de réessayer l'opération jusqu'à ce qu'il réussisse.

Ce qui est critique ici, c'est que je jette 'up' à IOException de ma part SavePost méthode qui permet à la politique Polly de gérer la réessayer. C'est un bon modèle à suivre car il vous permet de gérer la logique de réessayer dans un endroit central et ne pas avoir à vous en soucier dans chaque méthode qui pourrait avoir besoin de réessayer une opération.

En général toujours gérer les exceptions où vous pouvez et vomir à un niveau plus élevé où vous pouvez les gérer de manière plus centralisée (ou les enregistrer). Cela peut aider à réduire la complexité de votre code et faciliter le traitement des exceptions d'une manière cohérente.

      private async Task OnChangedAsync(WaitForChangedResult e)
    {
       ...
        var retryPolicy = Policy
            .Handle<IOException>() // Only handle IO exceptions (like file in use)
            .WaitAndRetryAsync(5, retryAttempt => TimeSpan.FromMilliseconds(500 * retryAttempt),
                (exception, timeSpan, retryCount, context) =>
                {
                    activity?.Activity?.SetTag("Retry Attempt", retryCount);
                    // Log the retry attempt
                    logger.LogWarning("File is in use, retrying attempt {RetryCount} after {TimeSpan}", retryCount,
                        timeSpan);
                });

      ...

            // Use the Polly retry policy for executing the operation
            await retryPolicy.ExecuteAsync(async () =>
            {
              ...
                var blogService = scope.ServiceProvider.GetRequiredService<IBlogService>();
                await blogService.SavePost(blogModel);
               ...
            });
...
    }

Encore une fois, c'est un exemple de mon code interagissant avec un service extérieur, dans ce cas le système de fichiers. Où j'EXPÈTE certains types d'erreurs à se produire. Je les enregistre aussi en utilisant SérilogTraçage qui les envoie à Seq que THEN m'envoie un e-mail lorsque l'erreur est enregistrée afin que je puisse identifier tous les problèmes qui pourraient se produire.

Encore une fois, l'approche générale est de traiter les exceptions où vous pouvez, de les enregistrer lorsque vous ne pouvez pas et de vous assurer que vous avez un moyen de savoir ce qui se passe. Cela peut aider à s'assurer que votre application est résiliente et peut traiter tous les problèmes qui pourraient survenir.

Service d'email

Dans mon service d'email, j'utilise les deux CircuitBreaker un modèle et une politique de réessayer. La politique de réessayer est utilisée pour traiter le cas où le service d'email n'est pas disponible et le disjoncteur est utilisé pour traiter le cas où le service d'email est occupé ou surchargé.

Ces deux éléments sont importants dans le cas des courriels; SMTP est un protocole relativement lent et potentiellement peu fiable.

Ici, je m'occupe de SmtpExceptions, où quand une erreur vient du service SMTP il réessayera d'abord trois fois avec un retard entre chaque réessayer. Si le service n'est toujours pas disponible après les relevés (et deux autres pour un nouvel envoi), le disjoncteur s'ouvrira et cessera d'envoyer des courriels pendant une minute. Cela peut aider à empêcher le service d'email d'être surchargé (et mon compte étant bloqué) et améliorer les chances que l'email soit envoyé avec succès.

         // Initialize the retry policy
            var retryPolicy = Policy
                .Handle<SmtpException>() // Retry on any exception
                .WaitAndRetryAsync(3, // Retry 3 times
                    attempt => TimeSpan.FromSeconds(2 * attempt),
                    (exception, timeSpan, retryCount, context) =>
                    {
                        logger.LogWarning(exception, "Retry {RetryCount} for sending email failed", retryCount);
                    });

            // Initialize the circuit breaker policy
            var circuitBreakerPolicy = Policy
                .Handle<SmtpException>()
                .CircuitBreakerAsync(
                    5,
                    TimeSpan.FromMinutes(1), 
                    onBreak: (exception, timespan) =>
                    {
                        logger.LogError("Circuit broken due to too many failures. Breaking for {BreakDuration}", timespan);
                    },
                    onReset: () =>
                    {
                        logger.LogInformation("Circuit reset. Resuming email delivery.");
                    },
                    onHalfOpen: () =>
                    {
                        logger.LogInformation("Circuit in half-open state. Testing connection...");
                    });
            _policyWrap = Policy.WrapAsync(retryPolicy, circuitBreakerPolicy);
            
            

Ceci est ensuite utilisé dans ma boucle d'envoi d'email qui attend que de nouveaux messages soient ajoutés au canal puis essaye de les envoyer.

Cela utilise toute la functioanlity dans la politique enveloppée pour ajouter la résilience au processus d'envoi d'emails.

 while (await _mailMessages.Reader.WaitToReadAsync(token))
        {
            BaseEmailModel? message = null;
            try
            {
                message = await _mailMessages.Reader.ReadAsync(token);

                // Execute retry policy and circuit breaker around the email sending logic
                await _policyWrap.ExecuteAsync(async () =>
                {
                    switch (message)
                    {
                        case ContactEmailModel contactEmailModel:
                            await _emailService.SendContactEmail(contactEmailModel);
                            break;
                        case CommentEmailModel commentEmailModel:
                            await _emailService.SendCommentEmail(commentEmailModel);
                            break;
                    }
                });

                _logger.LogInformation("Email from {SenderEmail} sent", message.SenderEmail);
            }
            catch (OperationCanceledException)
            {
                break;
            }
            catch (Exception exc)
            {
                _logger.LogError(exc, "Couldn't send an e-mail from {SenderEmail}", message?.SenderEmail);
            }
        }

En conclusion

Polly est une bibliothèque puissante qui peut vous aider à ajouter de la résilience à vos applications. En utilisant Polly, vous pouvez gérer des retries, disjoncteurs, timeouts, limiteurs de taux, replis et couvertures dans votre application. Cela peut aider à s'assurer que votre application est fiable et peut traiter tous les problèmes qui pourraient survenir. Dans ce post, je viens vraiment de couvrir un aspect de Polly; les relevés, ce sont un mécanisme qui peut améliorer la résilience et la fiabilité de votre application. En utilisant Polly, vous pouvez gérer les relevés de manière cohérente et vous assurer que votre application peut gérer tous les problèmes qui pourraient survenir.

logo

©2024 Scott Galloway