NOTE: Apart from
(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
//Less than a minute
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.
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:
En esta aplicación uso Polly en múltiples lugares.
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.
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.
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.
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);
}
}
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.