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.

Avançado 45 min 25 pontos Leitura 0%

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

  1. 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
  1. 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
  1. 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
  1. Expor endpoint de observação no Sinatra
get "/dead-messages" do
  content_type :json
  dead_repo.all.to_json
end

Como testar

  1. Subir ambiente:
docker compose -f infra/docker-compose.yml up --build -d kafka notification-service
  1. 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}
  1. Ver logs do notification:
docker compose -f infra/docker-compose.yml logs -f notification-service
  1. 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.