Estratégias de retry com dead letter queue para dependências externas instáveis

1. Fundamentos: Por que dependências externas exigem retry e DLQ?

1.1. Natureza das falhas em chamadas externas (transientes vs. permanentes)

Dependências externas — APIs de terceiros, bancos de dados remotos, serviços de mensageria — falham de maneiras distintas. Falhas transientes (timeout, conexão recusada, throttling) são temporárias e podem ser resolvidas com repetição. Falhas permanentes (payload inválido, recurso inexistente, erro de autenticação) nunca terão sucesso, independentemente do número de tentativas. Ignorar essa distinção leva a desperdício de recursos e degradação do sistema.

1.2. O papel da resiliência: evitar cascata de falhas e degradação do sistema

Sem uma estratégia de retry controlada, uma falha em uma dependência externa pode travar threads, acumular conexões e derrubar todo o serviço. O padrão retry com dead letter queue (DLQ) isola o impacto, permitindo que o sistema continue processando mensagens válidas enquanto falhas persistentes são redirecionadas para análise posterior.

1.3. Introdução ao padrão Dead Letter Queue (DLQ) como mecanismo de isolamento

Uma DLQ é uma fila separada que armazena mensagens que falharam repetidamente. Em vez de descartar ou reter indefinidamente na fila principal, a DLQ funciona como um "hospital" para mensagens doentes — preservando o payload e o histórico de erros para depuração, reprocessamento ou notificação manual.

2. Projetando a arquitetura de retry com DLQ

2.1. Componentes essenciais: fila principal, fila de retry, DLQ e workers

A arquitetura típica envolve quatro elementos:

Fila Principal → Worker → Decisão de Falha → Fila de Retry (backoff) → DLQ
  • Fila principal: recebe mensagens originais
  • Worker: processa a mensagem e chama a dependência externa
  • Fila de retry: armazena mensagens para reprocessamento com atraso
  • DLQ: armazena mensagens que esgotaram as tentativas

2.2. Fluxo de mensagens: da fila principal até a DLQ após falhas consecutivas

text
1. Mensagem chega na fila principal
2. Worker tenta processar → falha transiente (timeout)
3. Mensagem é movida para fila de retry com contador=1
4. Após backoff, worker tenta novamente → falha transiente
5. Mensagem é movida para fila de retry com contador=2
6. Após backoff, worker tenta novamente → falha permanente
7. Mensagem é movida para DLQ com contador=3 e erro registrado

2.3. Decisões de design: número máximo de tentativas, janela de tempo e critérios de falha

Definir maxRetries = 3 e backoffBase = 2 segundos é comum, mas depende do SLA da dependência. Para APIs com rate limit, considere jitter para evitar thundering herd. Falhas 4xx (cliente) devem ir direto para DLQ; 5xx (servidor) merecem retry.

3. Estratégias de retry: configurando o comportamento de repetição

3.1. Retry imediato vs. retry atrasado (backoff exponencial e jitter)

Retry imediato é útil para falhas de rede rápidas, mas pode sobrecarregar a dependência. Backoff exponencial com jitter é preferível:

Atraso = min(base * 2^tentativa, maxDelay) + random(0, jitter)

Exemplo: base=1s, tentativa=2 → atraso=4s + jitter de até 1s.

3.2. Controle de tentativas por mensagem: contadores, cabeçalhos e metadados

Cada mensagem carrega metadados de retry:

{
  "payload": { "id": 123, "acao": "processar_pagamento" },
  "metadata": {
    "retryCount": 2,
    "maxRetries": 3,
    "lastError": "HTTP 503 - Service Unavailable",
    "lastAttemptAt": "2025-03-15T10:30:00Z"
  }
}

3.3. Diferenciação de falhas: quando retentar vs. quando enviar diretamente para DLQ

Critérios práticos:

Tipo de falha Ação
Timeout (transiente) Retry com backoff
HTTP 429 (rate limit) Retry com jitter + cabeçalho Retry-After
HTTP 4xx (exceto 429) DLQ imediata
HTTP 5xx Retry até maxRetries, depois DLQ
Exceção de conexão Retry imediato (1x), depois backoff

4. Implementando a Dead Letter Queue: roteamento e armazenamento

4.1. Critérios de movimentação para DLQ (falha permanente, exaustão de tentativas, timeout)

A DLQ recebe mensagens quando:
- retryCount >= maxRetries
- Erro classificado como permanente (ex.: 400 Bad Request)
- Timeout excede limite configurado (ex.: 30s)
- Payload corrompido ou schema inválido

4.2. Estrutura da mensagem na DLQ: payload original, histórico de erros e metadados

{
  "originalPayload": { "id": 123, "acao": "processar_pagamento" },
  "errorHistory": [
    { "attempt": 1, "error": "HTTP 503", "timestamp": "2025-03-15T10:28:00Z" },
    { "attempt": 2, "error": "HTTP 503", "timestamp": "2025-03-15T10:29:00Z" },
    { "attempt": 3, "error": "HTTP 400", "timestamp": "2025-03-15T10:30:00Z" }
  ],
  "finalDecision": "DLQ - falha permanente",
  "movedAt": "2025-03-15T10:30:05Z"
}

4.3. Políticas de retenção e notificações para a DLQ (alertas e dashboards)

Configure TTL (time-to-live) na DLQ — 7 dias é comum. Alertas devem disparar quando o volume da DLQ ultrapassar um limiar (ex.: 100 mensagens em 5 minutos). Dashboards mostram taxa de entrada na DLQ por tipo de erro.

5. Monitoramento e operação do pipeline de retry

5.1. Métricas chave: taxa de sucesso, contagem de retries, volume na DLQ

Métricas essenciais:
- success_rate: porcentagem de mensagens processadas com sucesso na primeira tentativa
- retry_rate: mensagens que precisaram de retry
- dlq_rate: mensagens movidas para DLQ
- avg_retry_latency: tempo médio entre tentativas

5.2. Estratégias de reprocessamento da DLQ: manual, automático com gatilhos, ou reenvio

Três abordagens comuns:

  1. Manual: operador revisa a DLQ e reenvia mensagens corrigidas
  2. Automático com gatilho: quando a dependência se recupera, um worker varre a DLQ e tenta reprocessar
  3. Reenvio programado: após correção do bug, mensagens são movidas de volta para a fila principal

5.3. Tratamento de dependências externas instáveis: circuit breaker e fallback integrados

Combine retry/DLQ com circuit breaker: se a taxa de erro da dependência ultrapassar 50% em 1 minuto, o circuit breaker abre e mensagens vão direto para DLQ sem tentativas, protegendo o sistema.

6. Boas práticas e armadilhas comuns

6.1. Evitar retry infinito e loops de repetição

Sempre defina um limite máximo de tentativas. Sem maxRetries, uma falha transiente pode gerar milhões de requisições inúteis. Use contador com expiração.

6.2. Cuidados com idempotência e duplicação de mensagens

Retry pode gerar duplicatas. Garanta que o processamento seja idempotente — use identificadores únicos (ex.: idempotencyKey) para que a dependência ignore requisições repetidas.

6.3. Versionamento de esquemas e compatibilidade com DLQ

Mensagens na DLQ podem ficar obsoletas se o schema mudar. Versionar o payload (ex.: "schemaVersion": 2) permite que workers mais novos processem mensagens antigas ou as rejeitem adequadamente.

7. Exemplo prático: cenário de integração com API externa instável

7.1. Configuração da fila principal e parâmetros de retry

# Configuração do worker (pseudo-código)
QUEUE_NAME = "pedidos-processamento"
RETRY_QUEUE_NAME = "pedidos-retry"
DLQ_NAME = "pedidos-dlq"
MAX_RETRIES = 3
BACKOFF_BASE_SECONDS = 2
MAX_BACKOFF_SECONDS = 60

7.2. Lógica de tratamento de erro e movimentação para DLQ

function processarMensagem(mensagem):
    try:
        resposta = chamarApiExterna(mensagem.payload)
        if resposta.status == 200:
            return SUCESSO
        elif resposta.status in [429, 503]:
            throw TransientError(resposta.body)
        else:
            throw PermanentError(resposta.body)
    catch TransientError as erro:
        mensagem.retryCount += 1
        if mensagem.retryCount >= MAX_RETRIES:
            moverParaDLQ(mensagem, erro)
        else:
            delay = calcularBackoff(mensagem.retryCount)
            agendarReprocessamento(mensagem, delay)
    catch PermanentError as erro:
        moverParaDLQ(mensagem, erro)

7.3. Fluxo de reprocessamento e recuperação de mensagens da DLQ

function reprocessarDLQ():
    mensagens = lerTodasMensagensDaDLQ()
    for mensagem in mensagens:
        if API_externa_esta_saudavel():
            mensagem.retryCount = 0
            publicarNaFilaPrincipal(mensagem)
        else:
            manterNaDLQ(mensagem, "API ainda instável")

Referências