Fluxo de Pagamento e Notificação
Resiliência e poison messages (intro)
Você identifica mensagens problemáticas e implementa defesa inicial no consumidor de notificação.
Nesta aula você vai
- Detectar e classificar mensagens inválidas no consumer
- Separar falhas transitórias de falhas irrecuperáveis
- Preparar base para estratégia de DLQ
Resiliência e poison messages (intro)
Objetivos
- Detectar e classificar mensagens inválidas no consumer
- Separar falhas transitórias de falhas irrecuperáveis
- Preparar base para estratégia de DLQ
Pré-requisitos
- Notification consumindo eventos de pagamento
- Kafka com tópicos ativos
- Noções de retry/backoff
Conceito
Poison message é a mensagem que sempre falha no consumidor por problema de payload ou regra irrecuperável. Se o sistema só faz retry cego, ele trava a partição e derruba throughput das mensagens boas.
A primeira defesa é classificar erro: transitório (vale retry) ou irrecuperável (deve ir para DLQ/log de descarte). Essa separação evita loop infinito e mantém o fluxo saudável.
Nesta aula, você implementa controle inicial de tentativas no notification-service e registra mensagens descartadas para análise posterior.
Estrutura de arquivos
services/notification-service/
app/consumers/payment_consumer.rb
app/retry/retry_policy.rb
app/repository/dead_message_repository.rb
docs/runbooks/notification-consumer.md
Passo a passo
- Criar política de retry (
app/retry/retry_policy.rb)
class RetryPolicy
MAX_RETRIES = 3
def self.retriable?(error)
error.is_a?(Timeout::Error) || error.message.include?("temporário")
end
end
- Criar repositório de mensagens mortas
class DeadMessageRepository
def initialize
@dead_messages = []
end
def save(topic:, key:, payload:, reason:)
@dead_messages << { topic: topic, key: key, payload: payload, reason: reason, saved_at: Time.now.utc.iso8601 }
end
def all
@dead_messages
end
end
- Aplicar classificação no consumer
attempts = Hash.new(0)
consumer.each_message do |message|
begin
process_message(message)
rescue => e
attempts[message.key] += 1
if RetryPolicy.retriable?(e) && attempts[message.key] <= RetryPolicy::MAX_RETRIES
sleep(2 ** attempts[message.key]) # backoff exponencial simples
retry
end
dead_repo.save(
topic: message.topic,
key: message.key,
payload: message.value,
reason: e.message
)
end
end
- Expor endpoint de observação no Sinatra
get "/dead-messages" do
content_type :json
dead_repo.all.to_json
end
Como testar
- Subir ambiente:
docker compose -f infra/docker-compose.yml up --build -d kafka notification-service
- Publicar mensagem inválida no tópico de pagamento:
docker compose -f infra/docker-compose.yml exec kafka \
kafka-console-producer.sh --bootstrap-server kafka:9092 --topic payment.failed.v1
Enviar payload inválido:
{"eventId":"x","orderId":123}
- Ver logs do notification:
docker compose -f infra/docker-compose.yml logs -f notification-service
- Consultar mensagens descartadas:
curl -s http://localhost:4567/dead-messages
Saída esperada: lista com payload inválido e motivo do descarte.
Dicas de projeto
- Defina limite de tentativas por mensagem.
- Registre payload original para análise forense.
- Separe exceções de infraestrutura e de domínio.
- Mantenha visibilidade com endpoint/metrics de descartes.
Erros comuns
- Retry infinito para erro estrutural de payload.
- Não registrar motivo do descarte.
- Bloquear loop de consumo em erro irrecuperável.
- Tratar poison message como "caso raro" sem monitoramento.
Resumo
Você implementou a defesa inicial contra poison messages no Notification Service, com classificação de falhas, retries controlados e registro de descarte. Isso cria base segura para evoluir para DLQ completa.