Criando chamadas HTTP resilientes utilizando Polly com HttpClient Factory

Alef Carlos
Bee Lab Academy
5 min readJan 12, 2019

--

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

https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly

--

--

Alef Carlos
Bee Lab Academy

Desenvolvedor por paixão, arquiteto de software por profissão.