Estratégias de retry com circuit breaker em integrações com terceiros

1. Por que falhas em integrações com terceiros são inevitáveis?

1.1. Natureza imprevisível de APIs externas

APIs de terceiros estão sujeitas a inúmeros fatores que fogem ao nosso controle: picos de tráfego, manutenções não anunciadas, problemas de infraestrutura, limitação de taxa (rate limiting) e variações de latência. Segundo estudos da AWS, cerca de 10% das requisições a APIs externas falham por razões transitórias. Ignorar essa realidade é convidar instabilidade para o sistema.

1.2. Diferença entre falhas transitórias e falhas permanentes

Nem toda falha merece uma nova tentativa. Falhas transitórias (timeouts, 503 Service Unavailable, 429 Too Many Requests) podem ser resolvidas com uma espera estratégica. Já falhas permanentes (400 Bad Request, 401 Unauthorized, 404 Not Found) indicam problemas que não serão resolvidos com repetições — retentar nesses casos apenas desperdiça recursos.

1.3. Impacto de falhas não tratadas

Uma integração que falha sem tratamento adequado pode causar degradação em cascata: threads bloqueadas, aumento de latência, consumo de memória e, no pior cenário, derrubar todo o sistema. Para o usuário final, isso se traduz em erros 500, timeouts e uma experiência frustrante.

2. Fundamentos da estratégia de retry

2.1. O que é retry e quando aplicá-lo

Retry é a repetição automática de uma requisição que falhou, na esperança de que a falha seja temporária. Deve ser aplicado para:
- Códigos HTTP 429 (rate limit)
- Códigos 5xx (erros do servidor)
- Timeouts de conexão e leitura
- Exceções de rede (DNS, conexão recusada)

2.2. Políticas de retry

Intervalo fixo: espera constante entre tentativas. Simples, mas pode sobrecarregar o servidor.

Tentativa 1: falha → espera 1s
Tentativa 2: falha → espera 1s
Tentativa 3: falha → espera 1s

Backoff exponencial: aumenta o intervalo progressivamente.

Tentativa 1: falha → espera 1s
Tentativa 2: falha → espera 2s
Tentativa 3: falha → espera 4s

Jitter: adiciona aleatoriedade ao intervalo para evitar o "thundering herd problem".

Tentativa 1: falha → espera 0.8s a 1.2s
Tentativa 2: falha → espera 1.6s a 2.4s

2.3. Limites de tentativas e idempotência

Defina um número máximo de tentativas (3 a 5 é comum). Mais importante: só retente se a operação for idempotente. Uma requisição GET é segura; um POST de criação de recurso pode gerar duplicatas se repetido sem controle.

3. Introdução ao padrão Circuit Breaker

3.1. Conceito e estados

O Circuit Breaker atua como um interruptor inteligente que monitora falhas e evita chamadas desnecessárias:

  • Fechado: requisições passam normalmente. Falhas são contadas.
  • Aberto: após um limiar de falhas, todas as requisições são recusadas imediatamente (sem chamar o terceiro).
  • Meio-aberto: após um tempo de espera, permite uma requisição de teste para verificar se o serviço se recuperou.

3.2. Prevenção de falhas em cascata

Quando um serviço externo está lento, threads ficam bloqueadas esperando respostas. O circuit breaker corta esse ciclo: ao abrir o circuito, as requisições falham rapidamente, liberando recursos e evitando que a falha se espalhe.

3.3. Métricas para acionamento

As métricas mais comuns para abrir o circuito:
- Taxa de erro: percentual de falhas em uma janela de tempo (ex: >50% nos últimos 30 segundos)
- Número de falhas consecutivas: ex: 5 falhas seguidas
- Latência: se o tempo de resposta ultrapassa um limiar (ex: >10s)

4. Combinando retry e circuit breaker: abordagem em camadas

4.1. Ordem de execução

A ordem padrão é: retry primeiro, circuit breaker depois. Primeiro tentamos recuperar de falhas transitórias com retry; se o problema persiste, o circuit breaker assume para proteger o sistema.

4.2. Configuração de limites

  • Retry: 3 tentativas com backoff exponencial (máx 8s de espera total)
  • Circuit breaker: abre após 5 falhas consecutivas (incluindo as tentativas de retry)
  • Tempo de espera do circuito: 30 segundos antes de tentar meio-aberto

4.3. Tratamento de erros

Quando o retry esgota todas as tentativas, o erro é repassado ao circuit breaker. Se o circuito estiver aberto, o erro é imediato (fallback). Se estiver fechado, o circuit breaker registra a falha e decide se abre ou não.

5. Implementação prática com código

5.1. Retry com backoff exponencial e jitter

funcao executarComRetry(requisicao, maxTentativas=3):
    para tentativa de 1 ate maxTentativas:
        try:
            resposta = requisicao.executar()
            se resposta.sucesso:
                retornar resposta
            senao se resposta.codigo em [429, 503, 504]:
                espera = calcularBackoff(tentativa)
                espera += random(0, espera * 0.1)  // jitter de 10%
                aguardar(espera)
            senao:
                retornar erro  // falha permanente
        catch TimeoutError:
            espera = calcularBackoff(tentativa)
            aguardar(espera)
    retornar erro "Max tentativas excedido"

funcao calcularBackoff(tentativa):
    base = 1 segundo
    return min(base * (2 ^ (tentativa - 1)), 8 segundos)

5.2. Circuit breaker simples com contagem de falhas

class CircuitBreaker:
    estado = "fechado"
    contagemFalhas = 0
    limiarFalhas = 5
    timeoutAbertura = 30 segundos
    ultimaFalha = null

    funcao executar(requisicao):
        se estado == "aberto":
            se (agora - ultimaFalha) > timeoutAbertura:
                estado = "meio-aberto"
            senao:
                retornar erro "Circuito aberto"

        try:
            resposta = requisicao.executar()
            se resposta.sucesso:
                se estado == "meio-aberto":
                    estado = "fechado"
                    contagemFalhas = 0
                retornar resposta
            senao:
                registrarFalha()
        catch:
            registrarFalha()

    funcao registrarFalha():
        contagemFalhas += 1
        ultimaFalha = agora
        se contagemFalhas >= limiarFalhas:
            estado = "aberto"

5.3. Integração dos dois padrões

funcao chamadaResiliente(requisicao):
    // Camada 1: Retry (3 tentativas com backoff)
    resultado = executarComRetry(requisicao, 3)

    se resultado.sucesso:
        // Sucesso: reseta contadores do circuit breaker
        circuitBreaker.registrarSucesso()
        retornar resultado
    senao:
        // Camada 2: Circuit breaker registra a falha
        circuitBreaker.registrarFalha()
        retornar resultado.erro

6. Monitoramento e alertas para operações resilientes

6.1. Métricas essenciais

  • Taxa de retry: quantas requisições precisaram de retry vs. total
  • Estado do circuit breaker: tempo em aberto, número de aberturas
  • Taxa de sucesso pós-retry: quantas requisições se recuperaram
  • Latência média com e sem retry

6.2. Logs estruturados

{
  "evento": "retry_attempt",
  "tentativa": 2,
  "max_tentativas": 3,
  "codigo_http": 503,
  "tempo_espera_ms": 2000,
  "servico": "api-pagamentos",
  "circuit_breaker_estado": "fechado"
}

6.3. Alertas recomendados

  • Circuit breaker aberto por mais de 5 minutos
  • Taxa de retry acima de 20% em janela de 1 minuto
  • Mais de 10 falhas consecutivas sem recuperação
  • Tempo médio de resposta dobrou em relação ao baseline

7. Armadilhas comuns e boas práticas

7.1. Retry em endpoints não idempotentes

Nunca retente automaticamente requisições POST que criam recursos sem um identificador único (idempotency key). Se necessário, implemente um mecanismo de deduplicação no cliente ou utilize cabeçalhos como Idempotency-Key.

7.2. Evitar retry infinito

Sempre defina um número máximo de tentativas e um tempo total máximo. Retry infinito pode mascarar problemas reais e consumir recursos indefinidamente.

7.3. Testes de caos

Simule falhas em ambiente de staging:
- Desligue o serviço terceiro temporariamente
- Injete latência artificial (ex: 30s de delay)
- Force respostas 429 e 503

Valide se o retry + circuit breaker se comportam como esperado e se os fallbacks funcionam.

Referências