Pular para o conteúdo

Retry

Em sistemas distribuídos, falhas parciais e transitórias são a norma: um pod reiniciando, um pico de latência, um rebalanceamento de partição, um 429/503 momentâneo. Falhar imediatamente na primeira tentativa desperdiça requisições que teriam sucesso segundos depois. Por outro lado, repetir cegamente pode piorar um incidente (retry storm) e corromper estado se a operação não for idempotente.

Envolver a chamada em uma política que, ao capturar uma exceção classificada como transitória, aguarda e tenta de novo, até um limite de tentativas.

O intervalo entre tentativas cresce exponencialmente: base * 2^(n-1). Ex.: 200ms, 400ms, 800ms, 1600ms. Evita marteladas em intervalo fixo e dá tempo para a dependência se recuperar.

Sem jitter, muitos clientes que falharam ao mesmo tempo tentam de novo no mesmo instante — o efeito thundering herd, que ressincroniza a carga e mantém a dependência sobrecarregada. O jitter espalha as tentativas adicionando aleatoriedade ao intervalo (full jitter: random(0, base * 2^(n-1))).

Só repita o transitório:

  • Repetir: timeouts de conexão, 503 Service Unavailable, 429 Too Many Requests (respeitando Retry-After), erros de rede.
  • NÃO repetir: 400 Bad Request, 401/403, 404, 422 — erros determinísticos que falharão igual em toda tentativa. Repetir é só desperdício e latência.

Se a primeira tentativa chegou ao servidor e teve efeito, mas a resposta se perdeu, o retry repete o efeito. Para operações não idempotentes (ex.: “debitar R$10”), isso duplica o efeito. Torne a operação idempotente com chave de idempotência (o servidor deduplica pela chave) ou use apenas em GET/PUT/DELETE naturalmente idempotentes.

  • Prós: absorve falhas transitórias de forma transparente, melhora a taxa de sucesso efetiva.
  • Contras: aumenta a latência (cada tentativa espera), pode amplificar sobrecarga (retry storm) e é perigoso sem idempotência. Retries também consomem threads/conexões enquanto esperam.
  • Em falhas não transitórias (erros de validação, autorização) — repetir não muda o resultado.
  • Em operações não idempotentes sem chave de idempotência.
  • Quando a latência adicional é inaceitável para o SLA da chamada.
  • Quando o Circuit Breaker já está aberto — não faz sentido tentar contra uma dependência conhecidamente morta.
@Configuration
public class RetryBeansConfig {
@Bean
public RetryRegistry retryRegistry() {
var config = RetryConfig.custom()
.maxAttempts(4)
.intervalFunction(IntervalFunction
.ofExponentialRandomBackoff(
Duration.ofMillis(200), 2.0, 0.5))
.retryOnException(ex ->
ex instanceof CallNotPermittedException == false
&& isTransient(ex))
.build();
return RetryRegistry.of(config);
}
private boolean isTransient(Throwable ex) {
if (ex instanceof HttpServerErrorException server) {
return server.getStatusCode().is5xxServerError();
}
if (ex instanceof HttpClientErrorException.TooManyRequests) {
return true;
}
return ex instanceof ResourceAccessException;
}
}
@Service
@RequiredArgsConstructor
public class PricingGateway {
private final RestClient restClient;
@Retry(name = "pricing")
public Price fetchPrice(String sku) {
return restClient.get()
.uri("/prices/{sku}", sku)
.retrieve()
.body(Price.class);
}
}

ofExponentialRandomBackoff(200ms, 2.0, 0.5) = intervalo base 200ms, multiplicador 2, com 50% de jitter. A anotação @Retry(name = "pricing") aplica a política; costuma ser composta por fora de um @CircuitBreaker de mesmo nome.

graph TD
    A[Chamada remota] --> B{Sucesso?}
    B -- sim --> Z[Retorna resultado]
    B -- não --> C{Falha transitória?}
    C -- não --> E[Propaga erro imediatamente]
    C -- sim --> D{Tentativas < max?}
    D -- não --> F[Esgotou: propaga / Fallback]
    D -- sim --> G[Espera backoff + jitter]
    G --> A
  • Retry sem backoff (loop imediato): martela a dependência e vira DoS acidental.
  • Retry sem jitter: thundering herd — todos os clientes tentam no mesmo instante.
  • Repetir erros determinísticos (4xx): latência e carga sem chance de sucesso.
  • Retry em operação não idempotente: efeitos duplicados (cobrança em dobro).
  • Retries aninhados em várias camadas (client + gateway + service): multiplicam-se — 4 x 4 x 4 tentativas. Defina o retry em uma camada.
  • Retry sem Circuit Breaker: continua tentando contra uma dependência morta, prolongando o incidente.
  1. Por que idempotência é pré-requisito para retry seguro?
Resposta

Porque uma tentativa pode ter chegado e produzido efeito no servidor, mas a resposta se perdeu (timeout). O retry então repete a operação. Se ela não for idempotente (ex.: debitar valor), o efeito é aplicado duas vezes. Idempotência — nativa ou via chave de idempotência que o servidor deduplica — garante que repetir não altere o resultado.

  1. Para que serve o jitter no backoff?
Resposta

Para evitar o thundering herd: sem aleatoriedade, todos os clientes que falharam simultaneamente tentam de novo no mesmo instante, ressincronizando a carga e mantendo a dependência sobrecarregada. O jitter espalha as tentativas no tempo adicionando um componente aleatório ao intervalo.

  1. Quais tipos de falha NÃO devem ser repetidos?
Resposta

Falhas determinísticas: 400 Bad Request, 401/403 (auth), 404, 422. Elas falharão igual em toda tentativa — repetir só adiciona latência e carga. Retry serve para falhas transitórias: 503, 429, timeouts e erros de rede.

  1. Por que retry e circuit breaker se complementam?
Resposta

O retry cobre falhas transitórias de curta duração. O Circuit Breaker detecta falhas persistentes e abre o circuito, fazendo os retries falharem rápido (ou nem executarem) em vez de martelar uma dependência morta. Sem o breaker, os retries prolongariam o incidente e consumiriam recursos inutilmente.