Pular para o conteúdo principal

Idempotent Consumer

Descrição​

O padrão Idempotent Consumer projeta consumidores de mensagens de forma que processar a mesma mensagem mais de uma vez produza exatamente o mesmo efeito que processá-la uma única vez. Como a maioria dos brokers oferece entrega at-least-once (pelo menos uma vez), duplicatas são inevitáveis; em vez de tentar eliminá-las na entrega, o consumidor passa a tolerá-las, alcançando processamento effectively-once.

No Enterprise Integration Patterns (Hohpe e Woolf), o padrão se chama Idempotent Receiver. O livro o apresenta como a resposta ao fato de uma mensagem poder chegar mais de uma vez, mesmo que o emissor a tenha enviado uma só vez, e descreve duas estratégias: detectar e descartar duplicatas explicitamente ou projetar a semântica das mensagens para que o reprocessamento seja inofensivo (por exemplo, mensagens que carregam o estado absoluto, e não um incremento).

O consumidor extrai de cada mensagem uma chave de deduplicação estável (um identificador atribuído pelo produtor ou uma chave de idempotência de negócio) e consulta um armazenamento persistente de chaves já processadas. Se a chave já existe, a mensagem é confirmada e descartada como duplicata, opcionalmente devolvendo o resultado registrado anteriormente. Caso contrário, a mensagem é processada e a chave é registrada na mesma operação atômica, e só então a mensagem é confirmada.

Há duas formas comuns de guardar os identificadores: em uma tabela dedicada de mensagens processadas (por exemplo, PROCESSED_MESSAGES, com chave primária composta por identificador do assinante e da mensagem, de modo que a inserção falha se a mensagem já foi processada) ou nas próprias entidades de negócio que o consumidor cria ou atualiza. Em caso de duplicata, a transação é revertida e a mensagem é ignorada.

No catálogo Patterns of Distributed Systems (Unmesh Joshi), o Idempotent Receiver trata o mesmo problema entre cliente e servidor de um cluster, quando o cliente não sabe se uma requisição sem resposta foi perdida ou processada e a reenvia. A solução é um registro de clientes: o servidor atribui a cada cliente um ID único e cada requisição carrega um número sequencial dentro desse cliente. O servidor guarda a resposta de cada requisição processada, indexada por cliente e número, e, ao receber uma repetição, devolve a resposta guardada sem executar a operação de novo. Em um cluster com Replicated Log, o registro de clientes e as respostas são replicados como qualquer outra entrada, para que um novo líder também reconheça duplicatas. Como o armazenamento não pode crescer sem limite, os registros expiram: respostas de requisições que o cliente já confirmou receber são descartadas, e clientes inativos por um tempo são removidos do registro (e precisam se registrar de novo).

Para que isso funcione sob concorrência, a correção é garantida pelo armazenamento de dados (restrição de unicidade, inserção condicional ou set-if-absent) e não por uma verificação seguida de escrita na aplicação. Quando o registro da chave e os efeitos de negócio são gravados na mesma transação, a variante é conhecida como Inbox, a contraparte do lado consumidor do Transactional Outbox.

Problemas que resolve​

  • Reenvios do produtor após timeout ou falha transitória de rede, que geram duas cópias da mesma mensagem no broker.
  • Reentrega pelo broker quando a confirmação (ack) se perde, o lock expira ou o consumidor cai antes de confirmar.
  • Consumidor que conclui a escrita no banco mas falha antes do ack, fazendo outra instância repetir a operação.
  • Efeitos colaterais indesejados de duplicatas: registros duplicados, cobrança em dobro do cliente, notificações repetidas.
  • Consumidores concorrentes (competing consumers) recebendo cópias da mesma mensagem ao mesmo tempo.
  • Em clusters replicados, clientes que reenviam uma requisição após timeout ou troca de líder, sem saber se ela foi aplicada, o que exige registrar clientes e respostas por número de requisição (visão de Joshi).
  • Em microsserviços, onde brokers com at-least-once são preferidos por confiabilidade, a reentrega é esperada: consumidores não idempotentes (por exemplo, um que subtrai o valor de um evento AccountDebited do saldo) calculariam resultados incorretos ao processar um duplicado.

Quando usar​

  • O consumidor lê de um broker com entrega at-least-once, o padrão da maioria dos brokers.
  • Reprocessar uma mensagem pode gerar resultados incorretos, como transações financeiras duplicadas, criação duplicada de recursos ou notificações repetidas.
  • Vários consumidores concorrentes processam o mesmo canal, aumentando a chance de duplicatas simultâneas.
  • Fora do contexto de mensageria: pipelines ETL que reprocessam dados, processamento de streams retomado a partir de checkpoint, jobs agendados que se sobrepõem ou reiniciam, e endpoints de webhook/HTTP que recebem entregas duplicadas.

Quando evitar​

  • As operações do consumidor já são naturalmente idempotentes (por exemplo, upsert por identificador de negócio, escrita de valor absoluto em vez de incremento, PUT em um recurso), tornando a contabilidade de deduplicação um custo sem benefício.
  • A carga de trabalho tolera os efeitos de duplicatas ocasionais e o custo de manter um armazenamento de deduplicação supera o impacto de uma duplicata.

Trade-offs​

GanhoCusto / Risco
Permite usar entrega at-least-once e retentativas com segurança, sem corromper dadosExige um armazenamento persistente de chaves processadas, com custo de armazenamento e uma escrita extra por mensagem
Duplicatas deixam de ser um risco de correção e passam a ser uma condição toleradaImplementar corretamente armazenamento, commit atômico e limpeza é propenso a erros
Suporta consumidores concorrentes sem processamento em dobroA escolha errada da chave (identificadores de transporte, timestamps, correlation ids) anula a deduplicação
Facilita a recuperação após falhas, já que reprocessar é seguroEfeitos externos que não participam da transação exigem um fluxo em duas fases (in-progress / completed) e reconciliação de registros pendentes

Considerações adicionais:

  • Prefira idempotência natural: projete operações como upserts ou use event-carried state transfer (a mensagem carrega o estado absoluto resultante) e reserve a deduplicação para o que não puder ser naturalmente idempotente.
  • Atomicidade: grave o marcador de deduplicação e os efeitos de negócio na mesma transação; gravar em passos separados abre uma janela em que uma queda deixa o efeito aplicado e a chave não registrada.
  • Concorrência: use restrição de unicidade ou escrita condicional atômica; o padrão "verifica e depois grava" em cache tem condição de corrida.
  • Múltiplos assinantes: em publish-subscribe com armazenamento compartilhado, use chave composta (identidade do consumidor + identidade da mensagem) para que um consumidor não suprima o processamento dos demais.
  • Ciclo de vida dos registros: defina um TTL maior que a janela máxima de reentrega do broker (tentativas de entrega, lock/visibilidade, TTL da mensagem) e considere reenvios manuais a partir de dead-letter queues.
  • Deduplicação do broker não substitui o padrão: recursos como a detecção de duplicatas do Azure Service Bus atuam no envio e dentro de uma janela limitada; ajudam a reduzir volume, mas não evitam reprocessamento após reentrega.
  • Ordenação: deduplicar não garante ordem; combine com sessões/partições ordenadas ou dados de versão/sequência para rejeitar mensagens obsoletas.
  • Conflito de conteúdo: se a chave já existe mas o conteúdo difere, envie para dead-letter e gere alerta em vez de descartar silenciosamente.
  • Observabilidade: registre a chave de deduplicação e o identificador de correlação em logs estruturados e acompanhe uma métrica de duplicatas detectadas; um aumento pode indicar problemas no produtor, janela de ack subdimensionada ou consumidores instáveis.
  • Propagação: repasse a chave de idempotência para os serviços chamados a jusante, para que cada camada possa deduplicar seu próprio trabalho.
  • Frameworks: frameworks de mensageria costumam oferecer essa funcionalidade pronta (deduplicação, retenção e limpeza), reduzindo o risco de implementação manual.

Diagrama​

O consumidor verifica a chave de deduplicação e, se ela for nova, grava a chave e os efeitos de negócio na mesma transação atômica antes de confirmar. Quando a mesma mensagem chega de novo, a chave já existe (garantido por restrição de unicidade no armazenamento) e o consumidor apenas confirma e descarta a duplicata, sem repetir os efeitos.

Padrões relacionados​

  • Guaranteed Delivery — a entrega garantida do canal é o que torna as duplicatas possíveis e o receptor idempotente necessário (EIP).
  • Command Message — comandos reentregues são o caso típico em que a idempotência importa (EIP).
  • Competing Consumers — vários consumidores concorrentes aumentam a chance de duplicatas simultâneas, que o Idempotent Consumer trata com segurança.
  • Retry — retentativas de produtores e consumidores são uma das principais origens de duplicatas; consumidores idempotentes tornam as retentativas seguras.
  • Publisher-Subscriber — cada assinante recebe sua própria cópia e precisa rastrear o processamento de forma independente.
  • Queue-Based Load Leveling — filas com entrega at-least-once exigem consumidores tolerantes a reentrega.
  • Sequential Convoy — complementa a deduplicação quando o processamento depende de ordem.
  • Compensating Transaction — etapas de compensação também podem ser reexecutadas e devem ser idempotentes.
  • Saga — passos de uma saga coordenados por mensagens dependem de participantes idempotentes.
  • Messaging (microservices) — estilo de comunicação assíncrona cujo broker at-least-once torna o consumidor idempotente necessário.
  • Transactional Outbox (microservices) — contraparte do lado produtor; o Inbox é a contraparte do lado consumidor.
  • Replicated Log — o registro de clientes e as respostas guardadas são replicados no log para sobreviver à troca de líder (Patterns of Distributed Systems).
  • Request Waiting List — o líder acompanha requisições pendentes enquanto aguarda o commit, e o cliente pode repeti-las (Patterns of Distributed Systems).
  • Event Sourcing — projeções que reprocessam eventos precisam aplicar cada evento uma única vez.

Referência​