Outbox, Kafka, consumidores idempotentes, uma DLT e uma sala de espera para o BoxOffice
order chama notification por HTTP antes de responder. O provedor de e-mail leva 50–500 ms e às vezes falha. Às 10:00 o show no estádio abre as vendas. O que pode dar errado?
order
notification
fã → gateway → order → POST /notifications → SMTP → … → 201
POST /notifications
201
A requisição termina depois do e-mail.
fã → sala de espera → order → orders + outbox (1 tx) → 201
orders
outbox
relay → Kafka order-events → notification → Mailpit
order-events
falhas → order-events-dlt
order-events-dlt
kafka
apache/kafka:4.3.1
kafka-init
kafka-ui
mailpit
kafka:9092
localhost:9094
--reset-offsets
INSERT order; COMMIT; e depois kafka.send(...) — uma queda no meio guarda um pedido sem o seu evento. Ao contrário, você anuncia um pedido que não existe.
INSERT order; COMMIT;
kafka.send(...)
acks=all
SELECT id, aggregate_id, event_type, payload::text AS payload FROM outbox WHERE published_at IS NULL ORDER BY created_at LIMIT 100 FOR UPDATE SKIP LOCKED
UPDATE ... published_at
SKIP LOCKED
Alternativa: CDC (o Debezium lê o WAL) — menos atraso, mais infraestrutura.
INSERT INTO orders (...) VALUES (...) ON CONFLICT (hold_id) DO NOTHING
hold_id
UNIQUE
200
Idempotência é uma decisão de projeto em cada salto: API, relay, consumidor.
INSERT INTO processed_events (event_id) ... ON CONFLICT DO NOTHING
E-mail enviado, commit falha → um segundo e-mail na reentrega. Efeitos externos precisam da chave de idempotência do provedor (eventId).
eventId
DefaultErrorHandler
ExponentialBackOffWithMaxRetries(3)
DeadLetterPublishingRecoverer
JacksonException
IllegalArgumentException
Alternativa não bloqueante: @RetryableTopic (tópicos de retentativa) — sem bloqueio, mas sem ordem.
@RetryableTopic
2 M de fãs, 60 mil assentos, um inventário que atende algumas centenas de reservas por segundo. Quem entra primeiro?
INCR
ZPOPMIN
POST /orders
X-Admission-Ticket
espera = posição / taxa
NOTIFY_MODE
k6 em modelo aberto (constant-arrival-rate); lag pelo kafka-consumer-groups.sh; atrasos por SQL.
constant-arrival-rate
kafka-consumer-groups.sh
Concorrência 1, 50 ms por e-mail, 100 pedidos/s por 60 s. Lag no fim? Tempo para drenar?
event-fanout
async-queue
spike
Forme uma hipótese no simulador e depois confira com o k6 nos seus próprios serviços.
Roteiro: (1) por que o e-mail precisa sair do caminho da requisição; (2) Kafka em modo KRaft; (3) o problema da escrita dupla e o outbox; (4) consumidor idempotente; (5) retentativas e DLT; (6) sala de espera; (7) o que medimos.