En este post vamos a tratar Polly, una librería muy común dentro del mundo de C#, no solo para las empresas, sino que la propia Microsoft la utiliza internamente, aquí, vamos a ver una introducción.
Índice
Como siempre ya sabéis que el código está disponible en GitHub.
1 - Que es Polly?
Polly es una librería open source la cual nos permite incluir configuración a las llamadas http para que estas sean más resilientes.
Sinceramente, la palabra resiliente es un palabro extraño, si estas acostumbrado a hablar en inglés, habrás escuchado resilient, que es su traducción en inglés, pero claro, que quiere decir? Pues que es tolerante a fallos.
Lo que quiere decir que podemos utilizar Polly para que nuestras apps sean tolerantes a fallos, o que se puede recuperar de esos.
Pero qué tipo de fallos?
Polly se encarga de controlar las llamadas HTTP que hacemos desde nuestras aplicaciones hacia otras.
Antes de continuar, también podemos configurar esta tolerancia con la base de datos u otros sistemas, pero el 99% de casos de uso que se ven es con llamadas http, por eso, en este blog nos vamos a centrar en esta modalidad.
La idea principal de Polly es asegurarnos de que todo funciona y que un pequeño error de red no va a hacer que la app entera se nos caiga.
Dentro de polly tenemos dos formas de ejecutar las configuraciones
1.1 - Aplicación individual de la tolerancia a fallos.
Básicamente una función en nuestro código que nos permite para cierta llamada tener una póliza especial, por ejemplo queremos que la llamada se intente 3 veces, o queremos que después de 3 segundos la llamada se cancele, cosas así.
1.2 - Resilient pipeline en Polly
Con esta opción lo que hacemos es crear múltiples pólizas y las podemos combinar de una forma muy sencilla.
Veremos un ejemplo más adelante.
2 - Utilizar Polly dentro de nuestra aplicación C#
Como siempre en estos casos, lo primero que tenemos que hacer es importar el paquete desde NuGet Polly
, y ahora ya lo podemos utilizar.
Para este ejemplo, estoy utilizando el template por defecto de Web.API, únicamente he puesto un contador de veces y si el contador no ha llegado a ese número pues lanza una excepción.
Obviamente esto es únicamente para testear, en producción no hagáis esto.
//This times is here just to fail the first call and be able to test polly
int times = 0;
app.MapGet("/best-endpoint", () =>
{
if (times == 0)
{
times++;
throw new Exception("just an example");
}
return "Ok";
})
.WithName("BestEndpoint")
.WithOpenApi();
Además tenemos un endpoint el cual realiza una llamada HTTP and endpoint que acabamos de crear:
app.MapGet("/polly-execution1", (IHttpClientFactory httpClientFactory) =>
{
HttpClient client = httpClientFactory.CreateClient("PollyExample");
return client.GetStringAsync("best-endpoint");
})
.WithName("PollyExample1")
.WithOpenApi();
Con esto ya podemos probar Polly
2.1 - Reintentar llamadas con Polly
La función más popular de polly es sin lugar a dudas la de re-intentar llamadas, tenemos varias formas de hacerlas, por ejemplo, podemos reintentar basándonos en el estatus code de la respuesta, o si salta una excepción en que tipo.
Algo muy importante es que podemos añadir tiempo de espera en las llamadas, y que este sea incremental, por ejemplo entre la primera y la segunda que espere 500ms entre la segunda y la tercera que espere 1 segundo, y así. Esto es importante porque si no esperamos y tenemos digamos 5 llamadas es posible que las 5 llamadas se realicen en menos de un segundo, y cuando el error es de red o que la app destino está colapsada, este retry no nos servirá de mucho, pero si esperamos, quizá sí.
app.MapGet("/polly-execution-retry", (IHttpClientFactory httpClientFactory) =>
{
HttpClient client = httpClientFactory.CreateClient("PollyExample");
AsyncRetryPolicy policy = Policy
.Handle<Exception>()
.WaitAndRetryAsync(3, retryTime =>
TimeSpan.FromSeconds(0.5 * retryTime));
return policy.ExecuteAsync(async () => await client.GetStringAsync("best-endpoint"));
})
.WithName("PollyExampleRetry")
.WithOpenApi();
En este caso estamos haciendo la póliza para que lo intente 3 veces si da un error.
2.2 - Circuit breaker with Polly
Circuit breaker también es bastante popular, aunque es usado principalmente cuando tenemos microservicios y creamos llamadas entre ellos. Porque en realidad -casi- nadie hace circuit breaker cuando llamas a un servicio de terceros, que para eso pagas.
Circuit breaker no es ni mas ni menos que cuando el servicio al que llamamos hace timeout cancelar el resto de ejecuciones al dicho servicio.
En el caso anterior, por ejemplo si la primera llamada tarda 10 segundos, y falla, la segunda 10 segundos y falla, para que vamos a hacer una tercera, cuarta o quinta?
Para simular este escenario vamos al endpoint al que estamos consumiendo, y simplemente le añadimos un thread sleep the 10 segundos:
app.MapGet("/best-endpoint", () =>
{
switch (times)
{
case 0:
times++;
throw new Exception("just an example");
case 1:
times++;
Thread.Sleep(10000);
throw new Exception("just an example");
default:
return "Ok";
}
})
.WithName("BestEndpoint")
.WithOpenApi();
Ahora vamos a crear una póliza de polly que sea con circuit breaker:
AsyncCircuitBreakerPolicy policyCircuitBreaker = Policy
.Handle<Exception>()
.CircuitBreakerAsync(2, TimeSpan.FromMinutes(10));
app.MapGet("/polly-execution-circuitbreaker", (IHttpClientFactory httpClientFactory) =>
{
HttpClient client = httpClientFactory.CreateClient("PollyExample");
return policyCircuitBreaker.ExecuteAsync(async () => await client.GetStringAsync("best-endpoint"));
})
.WithName("PollyExampleCircuitBreaker")
.WithOpenApi();
Nota: la inicialización de la póliza está fuera del endpoint, porque si la pones dentro, se crea cada vez.
En este caso vamos a comprobar que dicha llamada no falla 2 veces en un periodo de 1 minuto, pero si te ves valiente puedes crear thresholds avanzados royo que no haya 3 consecutivos en 60 segundos pero si hay uno en el medio que funciona todo va bien, etc.
Y si ejecutamos el endpoint vamos a ver como falla instantáneamente, esto es debido a que la póliza no tiene retry, por lo que fallará dos veces, la segunda tomando más tiempo, y cuando ejecutamos la tercera nos devuelve un error en el circuit breaker, el cual tardará el tiempo que le hemos indicado en volver a funcionar, en mi caso 10 minutos.
Tanto retry como circuit breaker son las que yo personalmente he visto más en códigos de producción.
2.3 - Poliza de fallback en Polly
Otra póliza que podemos encontrar es fallback que básicamente significa, si la llamada falla, haz otra cosa. Ya bien sea hacer un hardcode del resultado, o en producción lo más probable sería llamar a otro servicio, ya que cuando yo he visto fallback, ha sido durante migraciones.
app.MapGet("/polly-execution-fallback", (IHttpClientFactory httpClientFactory) =>
{
HttpClient client = httpClientFactory.CreateClient("PollyExample");
AsyncFallbackPolicy<string> fallback = Policy<string>
.Handle<Exception>()
.FallbackAsync("All good here, nothing to see");
return fallback.ExecuteAsync(async () => await client.GetStringAsync("best-endpoint"));
})
.WithName("PollyExampleFallback")
.WithOpenApi();
Básicamente si falla invoca lo que hemos indicado en el fallback, en este caso es un string, pero podría ser la llamada a otro método. Este es el resultado:
2.4 - Combinar pólizas con Polly
Finalmente lo que vamos a ver hoy es cómo combinar múltiples pólizas, si te has ido fijando en los nombres de los tipos (por eso es bueno usar tipos y no var…) los tipos eran FallbackPolicy
, CircuitBreakerPolicy
, etc, como hacemos si por ejemplo queremos reintentar 3 veces, pero con un tiempo de ejecución máximo de 3 segundos, vamos que si tarda mas de 3 segundos reintentemos.
Aquí es donde entra la configuración de las múltiples pólizas. Las cuales la creamos utilizando la resilient pipeline.
Nota: en versiones anteriores lo que se hacía era un Policy.Wrap(fallback, retrypolicy, etc)
; Pero esta forma me parece más limpia y clara.
Para ello creamos la pipeline con las pólizas que necesitamos:
app.MapGet("/polly-execution-resilience-pipeline", async (IHttpClientFactory httpClientFactory) =>
{
HttpClient client = httpClientFactory.CreateClient("PollyExample");
ResiliencePipeline<HttpResponseMessage> pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddRetry(new RetryStrategyOptions<HttpResponseMessage>()
{
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<Exception>()
.HandleResult(response => response.StatusCode >= HttpStatusCode.InternalServerError),
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential
})
.AddTimeout(TimeSpan.FromSeconds(3))
.Build();
await pipeline.ExecuteAsync(async token => await client.GetAsync("best-endpoint"));
})
.WithName("PollyExampleResilience")
.WithOpenApi();
3 - Implementar una póliza directamente al HttpClient con Polly
Microsoft en combinación con el equipo de Polly han creado un paquete llamado Microsoft.Extensions.Http.Resilience
el cual nos permite incluir este tipo de mecanismos al propio HttpClient
directamente. Y utilizando la misma lógica que hemos visto en el ejemplo anterior, ahora podemos crear la póliza en el httpclient, lo que significa que afectará a todas las llamadas y será usada siempre que se use dicho httpclient.
Para ello lo hacemos con AddReslicienceHandler
el cual espera un parámetro string que es un identificador, este identificador se usa en la información de telemetry si lo tenemos habilitado. Y luego un delegado Action para la configuración, la cual es la misma que hemos visto en el punto anterior.
builder.Services
.AddHttpClient("PollyExample", client =>
{
client.BaseAddress = new Uri("http://localhost:5005");
}).AddResilienceHandler("api-1", config =>
{
config.AddRetry(new HttpRetryStrategyOptions()
{
MaxRetryAttempts = 2,
BackoffType = DelayBackoffType.Exponential,
Delay = TimeSpan.FromMilliseconds(500)
});
config.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions()
{
SamplingDuration = TimeSpan.FromSeconds(5),
FailureRatio = 0.9,
MinimumThroughput = 5,
BreakDuration = TimeSpan.FromSeconds(10)
});
});
Con esto, da igual donde utilicemos el cliente, todos tendrán dichas pólizas, lo cual es muy muy bueno.
Por cierto, si te parece mucho texto, puedes utilizar addStandardResilienceHandler
, lo cual permite quitar bastante código, aunque es más limitado, no tienes un número infinito de pólizas que puedas aplicar, y únicamente permite una de cada tipo.:
builder.Services
.AddHttpClient("PollyExample", client =>
{
client.BaseAddress = new Uri("http://localhost:5005");
}).AddStandardResilienceHandler()
.Configure(options =>
{
options.Retry.MaxRetryAttempts = 2;
options.Retry.BackoffType = DelayBackoffType.Exponential;
options.Retry.Delay = TimeSpan.FromSeconds(500);
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(5);
options.CircuitBreaker.FailureRatio = 0.9;
options.CircuitBreaker.MinimumThroughput = 5;
options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(10);
});
4 - conclusión
Para terminar una pequeña conclusión, utilizar este tipo de librerías, o incluso crear la tuya propia es muy importante en el entorno empresarial, ya que es más común de lo que parece que un servicio este caído por 1 segundo, así que debemos tener esta funcionalidad en cuenta.
Dicho esto, Polly es una librería mucho más completa de lo que he puesto hoy aqui, aquí he tratado sus funcionalidades principales y una primera toma de contacto, que siendo sinceros para el 90% de casos de uso nos sirve. Pero si quieres ir más en detalle, te recomiendo que mires su documentación.