Criando chamadas HTTP resilientes utilizando Polly com HttpClient Factory
Bom dia. Boa tarde. Boa noite.
Com o advento da arquitetura de microserviço surgiu a necessidade de adicionar resiliências na comunicação entre os serviços de uma solução. Ou até mesmo de um serviço externo.
O que seria uma chamada resiliente ?
São chamadas que têm a capacidade de se recuperarem quando ocorre algum erro durante a requisição. Por exemplo: um timeout ou até um erro interno do servidor.
Nós utilizaremos o framework Polly para dar essas capacidades para nossas requisições. E vamos um pouco além, utilizaremos o pacote Microsoft.Extensions.Http.Polly para estender as funções utilizadas com HttpClient Factory.
Nesse artigo não explicarei a utilização e conceito do HttpClient Factory. Um artigo interessante é do Renato Groffe que pode ser acessado aqui.
A utilização pura do framework Polly pode se tornar complexa e não tão compreensível durante a implementação, por isso foi desenvolvida a biblioteca Microsoft.Extensions.Http.Polly para facilitar nossa vida.
WebAPI de simulação
Para simular os erros das chamadas criei uma WebApi e hospedei no Heroku.
https://simulacao-erros-api.herokuapp.com
Essa api tem 3 endpoints:
- api/example/success
- api/example/timeout/{timeout}/complete/{success}
- api/error/timeout/{code}/complete/{success}
As requisições feitas no endpoint example/success sempre retornan 200.
Para simular intermitência de timeout e erro crie os endpoints que recebem qual erro devo emitir e a partir de quantas chamadas deve retornar sucesso.
Por exemplo, quero que numa chamada estoure um timeout de 10 segundos, porém na 3º tentativa deve retornar 200.
Então basta realizarmos a seguinte chamada:
GET api/example/timeout/10/complete/2
Na primeira chamada a requisição demorará 10 segundos para responder e na segunda responderá quase que instantaneamente.
A mesma ideia se aplica ao endpoint de erro, porém aqui podemos especificar qual é o StatusCode do response.
GET api/example/error/401/complete/3
Cada requisição demorará 1s para completar. Só para podermos acompanhar os logs com mais clareza
Projeto de consumo da WebApi
O código fonte de referência está nesse repositório.
Criei um projeto do tipo Console que realiza diversas chamadas nos endpoints mencionados acima.
Para utilizar o Polly em conjunto com o Http Factory é necessário adicionar o NuGet package Microsoft.Extensions.Http.Polly.
dotnet add package Microsoft.Extensions.Http.Polly
A partir de agora podemos configurar o comportamento. Os códigos abaixo estão no projeto ConsoleApp.
Eu criei uma classe ApiService contendo as chamadas:
Como vocês podem ver eu recebo o HttpClient como injeção de dependência e realizo a configuração básica no construtor.
Agora temos de adicionar o ApiService no nosso container de dependências. No arquivo Startup.cs e no método ConfigureServices:
services.AddHttpClient<ApiService>();
Agora podemos utilizá-lo.
Testes sem resiliências
O arquivo HandlerHosted.cs contém todo o escopo e execução da nossa aplicação. Estou utilizando o Host Genérico para criar aplicações Console.
Para executar o projeto basta executar o comando abaixo no terminal:
dotnet run --project .\ConsoleApp\ConsoleApp.csproj
Veremos o output das chamdas realizadas. Vamos entendê-lo:
info: ConsoleApp.HandlerHosted[0]
Realizando chamada respondendo sucesso...
info: System.Net.Http.HttpClient.ApiService.LogicalHandler[100]
Start processing HTTP request GET https://localhost:5001/api/example/success/
info: System.Net.Http.HttpClient.ApiService.ClientHandler[100]
Sending HTTP request GET https://localhost:5001/api/example/success/
info: System.Net.Http.HttpClient.ApiService.ClientHandler[101]
Received HTTP response after 284.2068ms - OK
info: System.Net.Http.HttpClient.ApiService.LogicalHandler[101]
End processing HTTP request after 297.1072ms - OK
info: ConsoleApp.HandlerHosted[0]
Chamado respondendo sucesso... OK
A primeira chamada executada é Sending HTTP request GET https://localhost:5001/api/example/success/
E como esse endpoint retorna sempre 200, o resultado foi o esperado Received HTTP response after 284.2068ms — OK
Vamos validar a próxima chamada:
info: ConsoleApp.HandlerHosted[0]
Realizando chamada respondendo timeout de 10 segundos e respondendo com sucesso na 2 tentativa...
info: System.Net.Http.HttpClient.ApiService.LogicalHandler[100]
Start processing HTTP request GET https://localhost:5001/api/example/timeout/15/complete/2
info: System.Net.Http.HttpClient.ApiService.ClientHandler[100]
Sending HTTP request GET https://localhost:5001/api/example/timeout/15/complete/2
info: System.Net.Http.HttpClient.ApiService.ClientHandler[101]
Received HTTP response after 15057.9186ms - OK
info: System.Net.Http.HttpClient.ApiService.LogicalHandler[101]
End processing HTTP request after 15058.04ms - OK
info: ConsoleApp.HandlerHosted[0]
Chamado respondendo timeout... OK
Essa chamada já é a tentativa de simulação de timeout, como não configuramos nenhuma regra ela retornou 200: Received HTTP response after 15057.9186ms — OK
E a próxima chamada é de simulação de erro:
Realizando chamada respondendo erro com o status 500 e respondendo com sucesso na 3 tentativa...
info: System.Net.Http.HttpClient.ApiService.LogicalHandler[100]
Start processing HTTP request GET https://localhost:5001/api/example/error/500/complete/3
info: System.Net.Http.HttpClient.ApiService.ClientHandler[100]
Sending HTTP request GET https://localhost:5001/api/example/error/500/complete/3
info: System.Net.Http.HttpClient.ApiService.ClientHandler[101]
Received HTTP response after 1023.4717ms - InternalServerError
info: System.Net.Http.HttpClient.ApiService.LogicalHandler[101]
End processing HTTP request after 1023.5098ms - InternalServerError
info: ConsoleApp.HandlerHosted[0]
Chamado respondendo error... OK
Como podemos ver simulamos um erro 500 e nos foi retornado exatamente o mesmo Received HTTP response after 1023.4717ms — InternalServerError.
A chamada de erro não foi resiliente, pois deu erro e simplesmente ignorou, não teve nenhum retry.
Adicionando resiliência
Vamos adicionar 3 retries caso ocorra algum erro durante a requisição, inclusive Timeout.
Alteramos o Startup do projeto para adicionar as regras de comportamento do Polly.
Vamos entender por partes.
O método GetRetryPolicy tem o build da nossa regra:
HandleTransientHttpError — adicona o manuseio de comportamento para os seguintes erros:
- Falhas de rede(
System.Net.Http.HttpRequestException
) - HTTP 5XX (erros de servidor)
- HTTP 408 (timeout da requisição)
.OrResult — adiciona uma configuração personalizada, nesse exemplo estamos pedindo também para realizar novas tentativas quando o response for 401 ou 404.
Or<TimeoutRejectedException> — adiciona o manuseio de comportamento para regras de Timeout do Polly. Utilizando em conjunto de Polly.Timeout.
WaitAndRetry — comportamento de quantos retries devemos fazer e qual é o período de espera entre as chamadas.
O trecho abaixo configura o tempo, em segundos, do tempo de timeout da requisição. Essa configuração é independente do timeout definido no HttpClient.Timeout.
//Configurando policy de timeout padrãovar timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(10);
E então adicionamos as configurações nesse nosso serviço.
//Add servicesservices.AddHttpClient<ApiService>() .AddPolicyHandler(GetRetryPolicy()) .AddPolicyHandler(timeoutPolicy);
Ao executarmos veremos agora que além da chamada atual, caso ocorra erro tentará mais 3 vezes até ter sucesso na requisição. Então serão realizadas no máximo 4 chamadas no total.
await DoSuccessAsync();await DoTimeoutAsync();await DoErrorAsync(500, 3);await DoErrorAsync(401, 2);await DoErrorAsync(404, 2);//Esse nunca irá completar, pois o número de tentativas com sucesso é 10. E cofiguramos no máximo 3 retries.await DoErrorAsync(500, 10);
O fluxo de execução do projeto tenta realizar as chamadas acima. Vamos analisar um output de erro para entendermos:
Podemos ver no output acima que as 2 primeiras tentativas retornaram InternalServerError, como o esperado. E na 3º o resultado foi Ok, pois na chamada foi informado para que desse certo nesse momento:
https://localhost:5001/api/example/error/500/complete/3
Retorne 500 enquanto número de chamadas for menor que 3.
Já o output do timeout:
Vemos que foi realizada 2 vezes a chamada e na segunda o resultado foi Ok e o tempo total da das requisições foi de 11.017ms.
Então com isso podemos configurar a resiliências de nossas requisições.
O código fonte de referência está nesse repositório. Até mais !
Referências
https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory