02 - At-least-once
video: https://youtu.be/... no frontmatter da nota.At-least-once
At-least-once é a garantia de entrega mais usada em sistemas de produção: cada mensagem é entregue ao menos uma vez, e duplicatas são possíveis quando falhas acontecem no pior momento. O mecanismo é o ciclo de confirmação: o consumidor processa e envia o ack, e o broker reentrega qualquer mensagem cuja confirmação não chegou dentro do prazo. A robustez vem justamente daí: nenhuma mensagem se perde silenciosamente por falha de rede, crash de consumidor ou reinício de deploy, porque tudo o que não foi confirmado volta. O preço é lógico e inevitável: se o processamento concluiu e o ack se perdeu, a mensagem será processada duas vezes, e o consumidor precisa ser idempotente para que a repetição não vire efeito colateral duplicado. Em produção, essa é a combinação realista dominante, at-least-once no transporte com idempotência na aplicação, e entender por que a duplicata é inevitável, e não um bug, é o que separa desenhos resilientes de desenhos que quebram na primeira falha real.
Conceito e semântica formal
A semântica promete no mínimo uma entrega por mensagem, sem prometer no máximo uma. O contrato cobre a perda, não a repetição: entre entregar uma vez e entregar duas, qualquer resultado é aceitável pelo transporte, e o custo de eliminar a segunda entrega é transferido para a camada de aplicação.
A posição da garantia na escada é o meio termo deliberado. Ela evita a perda silenciosa do at-most-once sem pagar o preço completo do exactly-once, que exige transações entre produtor, broker e consumidor. Para a maioria dos fluxos de negócio, o par at-least-once mais idempotência entrega o mesmo resultado observável de exactly-once, com complexidade muito menor.
A formalização útil para decisões: at-least-once é garantia de transporte, não de efeito. O efeito colateral exato, uma cobrança, um desconto de estoque, um e-mail, é responsabilidade do consumidor, e nenhuma configuração de broker substitui essa responsabilidade.
A garantia é o padrão de fábrica nos brokers de produção. RabbitMQ entrega com ack manual por desenho; SQS reentrega o que não recebe delete dentro do visibility timeout; Kafka redeliver o que está além do último offset commitado do grupo. Em todos eles, at-least-once é o que sai da caixa, e as outras semânticas são configurações que se aproximam ou se afastam desse centro.
O problema que resolve
O problema raiz é a perda por falha no pior momento. Considere o consumidor que processa a cobrança, grava a fatura e morre antes de confirmar. Sem reentrega, o cliente pagou e nada foi registrado; com at-least-once, o broker devolve a mensagem, o consumidor volta do estado persistido e conclui. O sistema se recupera sozinho, sem reconciliação manual.
O segundo problema é a operação contínua em ambiente que falha por desenho. Deploys reciclam processos no meio do processamento; escalas derrubam instâncias; redes separam consumidores do broker por segundos. Em qualquer janela dessas, mensagens em processamento ficam sem ack, e a reentrega é o que garante que nenhum trabalho escorrega pelo ralo.
O terceiro é a simplicidade operacional. A garantia exige apenas um mecanismo, o ack com requeue, presente em todo broker maduro. Não há configuração transacional, não há coordenação entre produtor e consumidor, e o comportamento sob falha é uniforme e previsível: o não confirmado volta.
| Origem da duplicata | Momento | Exemplo | Defesa típica |
|---|---|---|---|
| Retry do produtor | Publicação | Timeout após envio, envio repetido | Idempotent producer, dedup no broker |
| Reentrega do broker | Consumo | Crash entre processar e ack | Chave de idempotência no consumidor |
| Rebalanceamento | Consumer group | Partição muda de dono, offset volta | Commit explícito pós-processamento |
| Reprocessamento humano | Operação | Replay de DLQ ou do log | Mesma chave de idempotência |
A tabela evidencia o ponto central do padrão: duplicatas nascem em quatro frentes distintas, e nenhuma configuração única cobre todas. A chave de idempotência no consumidor é a única defesa que atravessa todas as frentes, porque atua no último ponto do caminho, onde o efeito acontece.
Funcionamento interno: ack, requeue e reentrega
O ciclo de vida da mensagem sob a garantia:
- O broker entrega a mensagem e marca a entrega, invisibilizando o envelope para os demais consumidores.
- O consumidor processa, executando todo o efeito colateral de negócio.
- O consumidor envia o ack, e o broker remove a mensagem definitivamente.
- Sem ack no prazo, o broker devolve a mensagem à fila e a entrega novamente, a outro consumidor ou ao mesmo.
A duplicata nasce exatamente na fronteira do passo 3: processou, morreu antes do ack, e a reentrega é certa. Não há protocolo que elimine a janela sem transformar ack e processamento em uma única operação atômica, o que é o território das transações. A janela é pequena, mas em volume alto ela abre todos os dias.
A contagem de reentregas também é parte do mecanismo. Mensagens que voltam repetidamente por falha contínua precisam de limite, ou envenenam o fluxo; é daí que nasce a política de retry com máximo de tentativas e o encaminhamento para a DLQ, coberto no padrão correspondente.
# Consumidor at-least-once com idempotência por chave de negócio
def processar(mensagem):
evento = desserializar(mensagem.corpo)
chave = f"pagamento:{evento.id}" # chave de idempotência
with banco.transacao():
if banco.existe(chave): # já processado antes?
mensagem.ack() # duplicata: confirma e sai
return
faturar(evento) # efeito colateral único
banco.registra(chave, # marca no MESMO commit
resultado="faturado")
mensagem.ack() # só após o commit persistir
# A regra de ouro: registrar o processamento e executar o efeito
# colateral no mesmo commit, ack sempre por último
Essa combinação é descrita frequentemente como effectively once, exatamente uma vez em efeito, sem exactly-once formal. O transporte repete, e o consumidor absorve a repetição; o resultado observável no negócio é o mesmo de uma única execução. É a solução dominante em produção porque não exige transação distribuída, apenas disciplina de commit local.
Idempotência: a contrapartida obrigatória
Idempotência é a propriedade de executar a mesma operação N vezes produzir o mesmo resultado que executá-la uma vez. Sob at-least-once, ela não é otimização, é pré-condição de correção: sem ela, a duplicata inevitável do transporte vira cobrança em dobro, estoque negativo e e-mail repetido.
A técnica canônica é a chave de idempotência processada no mesmo commit do efeito. A chave deriva de um identificador único da mensagem, message_id do broker, id do evento ou chave natural da operação, e a checagem e o efeito compartilham a transação. O erro clássico é checar antes e gravar depois em transações separadas, reabrindo a janela de duplicata entre a checagem e o registro.
Efeitos externos sem transação exigem camadas adicionais. Chamadas a gateways de pagamento aceitam chave de idempotência no header e rejeitam a repetição; e-mails podem ser suprimidos por registro de envio; atualizações de estado absoluto, como definir saldo igual a X, são naturalmente idempotentes e preferíveis a incrementos quando possível.
O exemplo canônico vem do mercado financeiro: gateways como Stripe e adquirentes em geral aceitam um header de idempotency key e garantem que o mesmo par chave e corpo de requisição produza uma única cobrança, repetindo a resposta da primeira. O padrão é a prova prática de que duplicata de entrega não precisa virar duplicata de efeito: o transporte entrega duas vezes, e o protocolo de aplicação cobra uma.
| Técnica | Onde aplicada | Quando usar | Limitação |
|---|---|---|---|
| Chave no mesmo commit | Banco do consumidor | Efeitos com transação | Exige armazenamento por chave |
| Chave de idempotência no gateway | Chamada externa | Pagamentos e APIs que suportam | Depende do provedor |
| Estado absoluto em vez de delta | Lógica de negócio | Atribuição de valores | Reescreve a lógica incremental |
| Deduplicação por janela | Fila ou cache | Alto volume com TTL curto | Perda de memória fora da janela |
| Registro de envio de comunicação | Notificações | E-mail, SMS, push | Crescimento ilimitado do log |
Deduplicação por janela
A deduplicação no broker é o complemento direto: o SQS FIFO mantém janela de deduplicação de cinco minutos por message deduplication id, descartando no broker a segunda cópia de envelope idêntico. O recurso é valioso e limitado: fora da janela, a duplicata retorna, e o consumidor continua precisando da sua própria defesa.
A janela também existe no lado produtor, no padrão conhecido como idempotent producer do Kafka, em que o broker elimina duplicatas causadas por retry do próprio produtor dentro de uma sessão. O mecanismo cobre a faixa mais comum de duplicação, o retry transparente, e não cobre reentregas de consumidor, que seguem sob responsabilidade da aplicação.
O desenho correto trata a deduplicação por janela como redução de volume de duplicatas, e não como garantia. Janela curta demais deixa passar repetições tardias; janela longa demais custa memória e estado. A combinação canônica em produção é broker com deduplicação de janela para cortar o ruído, e chave de idempotência no consumidor para o resto.
# Cache de deduplicação com TTL como primeira linha de defesa
vistos = TTLCache(maxsize=100_000, ttl=3600) # janela de 1 hora
def processar_com_cache(mensagem):
chave = mensagem.message_id
if chave in vistos: # corta a duplicata óbvia
mensagem.ack()
return
executar_e_registrar(mensagem) # efeito com transação própria
vistos[chave] = agora() # marca após o commit
mensagem.ack()
# O cache reduz reprocessamento imediato; a marca persistida no
# registro cobre a duplicata que voltar depois do TTL
Erros comuns
- Checagem de idempotência fora da transação do efeito: entre o existe e o grava, outra instância processa a mesma mensagem, e a duplicata escapa.
- Ack antes do commit persistir: o consumidor confirma, o processo morre, e o efeito colateral nunca acontece, quebrando a promessa da garantia.
- Confundir retry do produtor com reentrega do consumidor: a deduplicação de sessão cobre um, e não o outro, e a idempotência precisa cobrir os dois.
- Usar contadores incrementais sem proteção: incrementar saldo, estoque ou contador duas vezes é o erro mais frequente em fluxos at-least-once.
- Chave de idempotência com colisão natural: usar data e usuário como chave agrupa operações distintas e descarta processamentos legítimos.
- Consumir a mesma mensagem por filas paralelas sem coordenação: dois fluxos distintos processam o mesmo evento por assinaturas diferentes, e a deduplicação local de cada um não enxerga a do outro.
Boas práticas
- Padronize a chave de idempotência no contrato da mensagem, com origem clara do identificador, para que todos os consumidores deduplicuem da mesma forma.
- Registre o processamento e execute o efeito no mesmo commit, e mande o ack sempre como última instrução do fluxo.
- Configure visibilidade e prazo de ack para o pior processamento real, evitando reentrega que atropela execução legítima em andamento.
- Combine deduplicação no broker, quando disponível, com defesa própria no consumidor, tratando a primeira como otimização e a segunda como garantia.
- Monitore taxa de reentrega e duplicatas detectadas, porque o crescimento súbito de reentregas é sintoma de timeout mal calibrado ou de falha sistêmica.
- Teste a duplicata explicitamente em CI, entregando a mesma mensagem duas vezes, para que a idempotência seja propriedade verificada, não suposta.
- Dimensione a armazenagem da chave de idempotência com política de expiração coerente com a janela máxima de reentrega possível do broker.
- Prefira chaves de negócio estáveis, derivadas do evento, em vez de identificadores gerados no reprocesso, que mudam a cada tentativa e invalidam a defesa.