Back to "Uso de Polly para retrocesos"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

ASP.NET C# Polly

Uso de Polly para retrocesos

Sunday, 15 September 2024

Introducción

Polly es una parte crítica del kit de herramientas de cualquier desarrollador.NET. Es una biblioteca que le permite definir políticas para el manejo de excepciones y reintentos en su aplicación. En este artículo, exploraremos cómo I utilizar Polly para manejar los reintentos en esta aplicación.

Polly

Aunque Polly lo hace muy bien, no es todo lo que puede hacer, es realmente un conjunto de herramientas para añadir resiliencia a sus aplicaciones. Ambos haciendo llamadas a servicios externos e internamente.

Estos son tomados de la Página principal de Polly y son los patrones principales que puede utilizar con Polly:

  • Inténtalo de nuevo si algo falla. Esto puede ser útil cuando el problema es temporal y puede desaparecer.
  • Interruptor de circuitos: Deje de intentar si algo está roto o ocupado. Esto puede beneficiarle al evitar perder el tiempo y empeorar las cosas. También puede apoyar el sistema para recuperarse.
  • Tiempo de espera: Renunciar si algo toma demasiado tiempo. Esto puede mejorar su rendimiento liberando espacio y recursos.
  • Limitador de tarifas: Limite el número de solicitudes que realice o acepte. Esto puede permitirle controlar la carga y evitar problemas o penalizaciones.
  • Haz otra cosa si algo falla. Esto puede mejorar su experiencia de usuario y mantener el programa funcionando. Hedging: Haz más de una cosa al mismo tiempo y toma la más rápida. Esto puede hacer su programa más rápido y más sensible.

Cómo uso Polly

En esta aplicación uso Polly en múltiples lugares.

AntecedentesTranslateService

Para iniciar mi servicio de traducción y comprobar los servidores EasyNMT están disponibles. Thsi me permite comprobar que el servicio está disponible antes de empezar a 'ofrecer' el servicio de traducción en mi aplicación. Recordarás que ambos se usan para mi editor 'juguete' para permitirle traducir Markdown y para mi 'en la marcha' motor de traducción de post blog. Así que es crítico comprobar que EasyNMT no ha caído (y habilitar la espera hasta que aparezca; lo que puede tomar unos segundos).

 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;
        }
    }

Aquí puede ver que establecemos una política de Polly Retry que se reintentará 3 veces con una espera de 5 segundos entre cada reintento. Si el servicio todavía no está disponible después de los reintentos, registramos un error y manejamos el fallo configurando el TranslationServiceUp bandera a falso. Esto permite que cualquier servicio que utilice el servicio de traducción sepa que no está 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

También uso Polly en mi biblioteca Umami.Net para manejar los reintentos cuando hago peticiones a la API de Umami. Esta es una parte crítica de la biblioteca, ya que me permite manejar cualquier problema con la API y volver a probar la solicitud si es necesario.

Aquí preparé mi HttpClient para usar una política de reintento; en este caso estoy comprobando HttpStatusCode.ServiceUnavailable y volver a intentar la solicitud si se produce. También estoy usando un Decorrelated Jitter Backoff estrategia para esperar entre reintentos. Esta es una buena estrategia para usar, ya que ayuda a evitar el problema del "rebaño que se tambalea" donde todos los clientes vuelven a intentar al mismo tiempo (a pesar de que sólo soy yo :)). Esto puede ayudar a reducir la carga en el servidor y mejorar las posibilidades de que la solicitud tenga éxito.

     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);
    }

Uso de una política de reintento para HttpClient las solicitudes son una forma importante de mejorar la fiabilidad. Mientras que nos gusta pensar que nuestros servicios web están siempre disponibles siempre hay ALGO tiempo de inactividad (en mi caso cuando por ejemplo Watchtower detecta que está pendiente una actualización y reinicia el contenedor de Umami). Así que tener una política de reintentar en el lugar puede ayudar a asegurar que su aplicación puede manejar estas situaciones con gracia.

FileSystemWatter

Otro uso que hago de Polly es cuando se trata de cargar y guardar archivos en mi aplicación. Utilizo un FileSystemWatcher para monitorear el directorio donde se almacenan mis archivos Markdown. Cuando se crea o actualiza un archivo, se carga el archivo y se procesa. Esto puede ser un problema si el archivo todavía está siendo escrito cuando se activa el evento. Así que uso un RetryPolicy para manejar esta situación.

Aquí puedes ver que me encargo de la IOException que se lanza cuando el archivo está en uso y volver a probar la operación. Utilizo un WaitAndRetryAsync política de reintentar la operación 5 veces con un retraso entre cada reintento. Esto me permite manejar la situación en la que el archivo todavía está siendo escrito y volver a probar la operación hasta que tenga éxito.

Lo que es crítico aquí es que vomito 'para arriba' IOException de mi SavePost método que permite a la política de Polly manejar el reintento. Este es un buen patrón a seguir ya que le permite manejar la lógica de reintentar en un lugar central y no tiene que preocuparse por ello en cada método que podría necesitar para reintentar una operación.

En general siempre manejar excepciones donde se puede y vomitar a un nivel más alto donde puedas manejarlos de una manera más centralizada (o registrarlos). Esto puede ayudar a reducir la complejidad de su código y hacer que sea más fácil manejar las excepciones de una manera consistente.

      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);
               ...
            });
...
    }

De nuevo este es un ejemplo de mi código interactuando con un servicio externo, en este caso el sistema de archivos. Donde ESPERO que ocurran ciertos tipos de error. También registro estos usando SerilogTracing que los envía a Seq que ENTONCES me envía un correo electrónico cuando el error está registrado para que pueda identificar cualquier problema que pueda estar ocurriendo.

De nuevo, el enfoque general es manejar excepciones donde puedas, registrarlas cuando no puedas y asegurarte de tener una manera de saber lo que está pasando. Esto puede ayudar a asegurar que su aplicación es resistente y puede manejar cualquier problema que pueda ocurrir.

Servicio de correo electrónico

En mi servicio de correo electrónico uso tanto el CircuitBreaker patrón y una política de reintento. La política de reintentar se utiliza para manejar el caso en el que el servicio de correo electrónico no está disponible y el interruptor se utiliza para manejar el caso en el que el servicio de correo electrónico está ocupado o sobrecargado.

Ambos son importantes en el caso de los correos electrónicos; SMTP es un protocolo relativamente lento y potencialmente poco fiable.

Aquí me ocupo de SmtpExceptions, donde cuando un error viene del servicio SMTP primero volverá a intentar tres veces con un retraso entre cada reintento. Si el servicio todavía no está disponible después de los reintentos (y dos más para un nuevo envío), el interruptor se abrirá y dejará de enviar correos electrónicos por un minuto. Esto puede ayudar a evitar que el servicio de correo electrónico se sobrecargue (y que se bloquee mi cuenta) y mejorar las posibilidades de que el correo electrónico se envíe con éxito.

         // 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);
            
            

Esto se utiliza en mi bucle de envío de correo electrónico que espera a que se añadan nuevos mensajes al canal y luego intenta enviarlos.

Esto utiliza toda la funcionalidad en la política envuelta para añadir resiliencia al proceso de envío de correo electrónico.

 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);
            }
        }

Conclusión

Polly es una poderosa biblioteca que puede ayudarle a añadir resiliencia a sus aplicaciones. Mediante el uso de Polly puede manejar reintentos, interruptores, tiempos de espera, limitadores de velocidad, retrocesos y cobertura en su aplicación. Esto puede ayudar a asegurar que su aplicación es confiable y puede manejar cualquier problema que pueda ocurrir. En este post realmente acabo de cubrir un aspecto de Polly; reintentos, estos son un mecanismo que puede mejorar la resiliencia y la fiabilidad de su aplicación. Usando Polly puedes manejar los reintentos de una manera consistente y asegurarte de que tu aplicación pueda manejar cualquier problema que pueda ocurrir.

logo

©2024 Scott Galloway