Resiliência à Prova de Falhas no .NET 10 com Polly
No desenvolvimento de software moderno, especialmente em arquiteturas de microsserviços e nuvem, a estabilidade não é definida pela ausência de falhas, mas pela capacidade do sistema de resistir e se recuperar delas.
Com a chegada do .NET 10 (e seguindo a evolução desde o .NET 8), a forma de implementar resiliência mudou drasticamente. As antigas Policies síncronas e pesadas deram lugar aos Resilience Pipelines: leves, performáticos e nativos para código assíncrono.
Este artigo explora como utilizar o poder do Polly em conjunto com o Microsoft.Extensions.Http.Resilience para criar aplicações robustas.
1. O Novo Paradigma: Resilience Pipelines
Diferente das versões anteriores do Polly (v7 e inferiores), a nova arquitetura (v8+) foca em:
- Zero-Allocation (ou quase): Redução drástica no consumo de memória.
- Pipeline Pattern: Estratégias são encadeadas de forma lógica e fluente.
- Integração Nativa: Telemetria, Logging e Injeção de Dependência já "saem da caixa".
Instalação
Para projetos .NET modernos, você deve instalar os seguintes pacotes via NuGet:
dotnet add package Polly.Core
dotnet add package Microsoft.Extensions.Http.Resilience
Nota: O pacote
Microsoft.Extensions.Http.Resilienceé a recomendação oficial da Microsoft para aplicações web e APIs.
2. Estratégias Fundamentais de Resiliência
Abaixo, detalhamos as principais estratégias (strategies) utilizando a sintaxe moderna do ResiliencePipelineBuilder.
A. Retry (Tentativa) com Jitter
A estratégia mais básica. Se falhar, tente novamente. No entanto, o Retry simples é perigoso (pode causar efeito "Thundering Herd"). A solução é o Backoff Exponencial com Jitter (ruído aleatório), que agora é o padrão no Polly moderno.
using Polly;
using Polly.Retry;
ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
// Trata exceções de rede e códigos 5xx
ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>(),
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(2),
BackoffType = DelayBackoffType.Exponential, // Inclui Jitter automaticamente
OnRetry = static args =>
{
Console.WriteLine($"Tentativa {args.AttemptNumber} falhou. Retentando...");
return ValueTask.CompletedTask;
}
})
.Build();
B. Circuit Breaker (Disjuntor)
Protege o sistema de destino. Se um serviço está fora do ar, continuar tentando só piora a situação. O Circuit Breaker "abre o circuito" após um número de falhas, falhando imediatamente novas requisições para dar tempo ao serviço de se recuperar.
var circuitOptions = new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5, // Se 50% das requisições falharem...
SamplingDuration = TimeSpan.FromSeconds(30), // ...nos últimos 30s...
MinimumThroughput = 7, // ...com no mínimo 7 requisições...
BreakDuration = TimeSpan.FromSeconds(60) // ...abra o circuito por 1 min.
};
var pipeline = new ResiliencePipelineBuilder()
.AddCircuitBreaker(circuitOptions)
.Build();
C. Fallback (Plano de Contingência)
Quando tudo falha (Retry esgotado, Circuito aberto), o Fallback define o que fazer para não estourar erro na cara do usuário. Pode ser retornar um dado do cache ou um valor padrão.
var pipeline = new ResiliencePipelineBuilder()
.AddFallback(new FallbackStrategyOptions<UserDto>
{
FallbackAction = _ => Outcome.FromResult(new UserDto { Name = "Usuário Padrão" }),
OnFallback = static _ =>
{
Console.WriteLine("Fallback acionado! Retornando dados provisórios.");
return ValueTask.CompletedTask;
}
})
.Build();
D. Hedging (Especulação)
Útil para baixa latência. O Hedging dispara requisições paralelas (ou com pequeno atraso). A primeira que retornar com sucesso é usada; as outras são canceladas.
Exemplo: Você chama uma API que às vezes demora 5s, mas normalmente leva 100ms. O Hedging dispara uma segunda chamada se a primeira demorar mais que 200ms.
var pipeline = new ResiliencePipelineBuilder()
.AddHedging(new HedgingStrategyOptions
{
MaxHedgedAttempts = 2,
Delay = TimeSpan.FromMilliseconds(500), // Dispara 2ª tentativa se 1ª demorar 500ms
})
.Build();
3. A Implementação "Gold Standard" no .NET 10
Em aplicações reais (.NET Core API, Worker Services, Blazor), você raramente constrói pipelines manualmente. Você utiliza a Injeção de Dependência para decorar o HttpClient.
A Microsoft introduziu o AddStandardResilienceHandler, que aplica uma combinação pré-configurada e otimizada de:
- Rate Limiter
- Total Timeout
- Retry
- Circuit Breaker
- Attempt Timeout
Configuração no Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
// 1. Registro do HttpClient nomeado
builder.Services.AddHttpClient("CatalogoApi", client =>
{
client.BaseAddress = new Uri("https://api.catalogo.com");
client.Timeout = TimeSpan.FromSeconds(30); // Timeout do socket
})
// 2. Adicionando a Resiliência Padrão
.AddStandardResilienceHandler(options =>
{
// Podemos customizar os padrões se necessário
options.Retry.MaxRetryAttempts = 5;
options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(15);
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(60);
});
var app = builder.Build();
Consumindo o Serviço Resiliente
Sua classe de serviço não precisa saber que o Polly existe. A resiliência é transparente.
public class CatalogoService(IHttpClientFactory factory, ILogger<CatalogoService> logger)
{
private readonly HttpClient _client = factory.CreateClient("CatalogoApi");
public async Task<Produto?> GetProdutoAsync(int id, CancellationToken ct)
{
// Se a rede falhar, o Polly intercepta, tenta de novo,
// ou abre o circuito automaticamente aqui.
try
{
return await _client.GetFromJsonAsync<Produto>($"/produtos/{id}", ct);
}
catch (Exception ex)
{
// Logar apenas se todas as estratégias de resiliência falharem
logger.LogError(ex, "Não foi possível obter o produto após múltiplas tentativas.");
throw;
}
}
}
4. Cenários Avançados: Pipeline Registry
Às vezes, você precisa reutilizar a mesma estratégia de resiliência em vários lugares que não são necessariamente chamadas HTTP (ex: acesso a arquivos, conexões Redis). Para isso, usamos o ResiliencePipelineProvider.
Registro
builder.Services.AddResiliencePipeline("meu-pipeline-banco", builder =>
{
builder
.AddRetry(new RetryStrategyOptions { MaxRetryAttempts = 3 })
.AddTimeout(TimeSpan.FromSeconds(5));
});
Uso Injetado
public class BancoService(ResiliencePipelineProvider<string> pipelineProvider)
{
public async Task SalvarDadosAsync(string dados, CancellationToken ct)
{
// Recupera o pipeline pelo nome
var pipeline = pipelineProvider.GetPipeline("meu-pipeline-banco");
// Executa qualquer código arbitrário com resiliência
await pipeline.ExecuteAsync(async token =>
{
await _database.ExecuteCommandAsync(dados, token);
}, ct);
}
}
5. Checklist de Melhores Práticas
-
Idempotência é Chave: Nunca use Retry em métodos que não sejam idempotentes (ex:
POSTpara criar pedido) a menos que sua API suporte verificação de duplicidade. Se a primeira requisição criou o pedido mas a resposta falhou na rede, o Retry pode criar um pedido duplicado. - Timeouts em Camadas:
- Attempt Timeout: Tempo para cada tentativa individual (curto).
- Total Timeout: Tempo máximo para todo o processo (longo).
Socket Timeout: Configuração do
HttpClient.Circuit Breaker Distribuído: Lembre-se que o Circuit Breaker é local (na memória da instância). Se você tem 10 réplicas da sua API no Kubernetes, cada uma terá seu próprio estado de circuito.
Não trate tudo: Não use Retry para erros de negócio (
400 Bad Request,403 Forbidden). Resiliência é para falhas transitórias (503 Service Unavailable,408 Request Timeout, falhas de rede).
Conclusão
No .NET 10, a biblioteca Polly deixou de ser apenas um utilitário de terceiros para se tornar parte integrante da stack de rede da Microsoft. Ao utilizar Microsoft.Extensions.Http.Resilience e os novos Pipelines, você garante que sua aplicação seja capaz de suportar as instabilidades inerentes à nuvem com performance máxima e código limpo.
Top comments (0)