Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

02 - At-least-once

#ack#at-least-once#deduplicacao#duplicatas#idempotencia
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione 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:

  1. O broker entrega a mensagem e marca a entrega, invisibilizando o envelope para os demais consumidores.
  2. O consumidor processa, executando todo o efeito colateral de negócio.
  3. O consumidor envia o ack, e o broker remove a mensagem definitivamente.
  4. 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.