Integrações na prática

O mesmo evento duas vezes: a chave que impede o pedido gêmeo

Webhook reenviado, retentativa depois do tempo esgotado, lote liberado à mão. Onde mora a chave de idempotência e por que verificar antes de inserir tem corrida.

Leonardo Peron5 min de leitura

Toda integração que funciona vai, mais cedo ou mais tarde, receber o mesmo aviso duas vezes. Não é anomalia: é como os dois lados combinaram de conversar. O provedor reenvia o webhook até receber confirmação, a sua rotina repete a chamada que estourou o tempo, alguém libera um lote parado. Em algum desses caminhos nasce o pedido gêmeo — mesmo cliente, mesmo valor, dois registros —, e ele costuma aparecer no financeiro, não no log.

Por que o evento chega duas vezes

O outro lado reenvia. Quase todo provedor repete a entrega até receber um 2xx. Se a sua resposta demorou e ele desistiu de esperar, do lado dele a entrega falhou — mesmo com o pedido já criado do lado de cá.

Nós reenviamos. A chamada estourou o tempo limite e a rotina tentou de novo. Tempo esgotado é a pior resposta possível: não diz se a operação aconteceu.

Alguém reprocessa. Uma fila parada é liberada, um lote é reenviado à mão, a varredura de reconciliação traz de volta o que o webhook já trouxe.

Há um quarto caso, que não é reenvio: dois processos consumindo a mesma fila pegam a mesma mensagem ao mesmo tempo. Nada chegou duas vezes; está sendo tratado duas vezes, em paralelo. Guarde a distinção, porque é ela que derruba a solução mais popular.

A chave identifica o fato, não a tentativa

Idempotência é a propriedade de uma operação que produz o mesmo resultado sendo executada uma ou dez vezes. Ela se obtém com uma chave, e a chave é a decisão inteira.

Ela tem que identificar o fato: o pedido 1234 da loja foi pago. Não a mensagem, não a tentativa, não o instante. O defeito clássico é gerar um identificador novo a cada envio — cada retentativa carrega uma chave inédita, e a proteção existe no código sem existir na prática. Pelo mesmo motivo, chave derivada do horário de chegada não serve: muda entre duas entregas do mesmo fato.

Vale dar escopo à chave: pedido 1234 da loja e pedido 1234 do marketplace são fatos diferentes com o mesmo número.

Onde a chave mora

O arranjo que parece prático e não é: guardar as chaves já vistas num cache em memória, ou num Redis, e gravar o pedido no banco. São duas escritas em dois lugares, e entre elas existe um intervalo. Um reinício ali dentro deixa a chave marcada sem o pedido, ou o pedido sem a marca. O primeiro caso é o silencioso: o pedido não existe e nada vai tentar criá-lo de novo.

A chave vai na mesma transação e no mesmo armazenamento do registro que ela protege, com restrição de unicidade na coluna.

"Verificar antes de inserir" tem corrida

Esta é a solução mais comum e mais frágil: consultar se o pedido já existe e, não existindo, inserir.

Entre a consulta e a inserção há uma janela. Dois consumidores da mesma fila consultam ao mesmo tempo, os dois não encontram nada, os dois inserem. A janela é curta, e é justamente esse o problema: ela só se manifesta sob concorrência, que é quando a integração está no pico. Em teste, com um evento por vez, o desenho passa sempre.

O banco resolve isso e o código não. Com índice único sobre a chave, a inserção duplicada é recusada pelo próprio banco, e o tratamento é capturar a violação e seguir como sucesso. A restrição é o único ponto do sistema onde verificar e escrever são a mesma operação.

Quando o destino é uma API de terceiro sem chave de idempotência — e alguns ERPs não oferecem —, a defesa fica antes da chamada, com a mesma restrição.

Guardar o resultado, não só a marca

Marcar a chave como vista e devolver sucesso vazio funciona enquanto ninguém precisa do retorno. Se quem chamou espera o identificador criado no destino, a segunda chamada devolve sucesso sem o número, e o chamador conclui alguma coisa errada — normalmente que precisa criar de novo. O desenho completo guarda chave e resultado: a repetição recebe de volta o mesmo resultado da primeira vez, sem executar nada.

Quando a origem não manda identificador estável

Acontece: o aviso chega sem identificador próprio, ou com um número novo a cada entrega. O caminho é montar a chave a partir do conteúdo, escolhendo os campos que identificam o fato — origem, tipo do evento, identificador do pedido e o estado que ele passou a ter. Escolher, não somar tudo: resumir a mensagem inteira num hash é frágil, porque qualquer campo de horário ou versão do formato produz um hash diferente, e a proteção para de funcionar sem avisar.

Existe o risco oposto, pior de perceber. Se dois fatos legítimos podem ser idênticos nos campos escolhidos — mesmo cliente, mesmo valor, mesmo minuto, duas compras de verdade —, a chave por conteúdo funde os dois num só. Duplicar é visível; engolir um pedido real não é. Quando os campos não separam os dois casos, é hora de pedir um identificador à origem em vez de adivinhar.

O teste que confirma

Dispare o mesmo webhook três vezes seguidas e confirme que existe um registro só. Isso é o mínimo, e quase toda integração passa.

O teste que separa é outro: dispare duas cópias ao mesmo tempo, em paralelo, não em sequência. É ele que exercita a corrida entre consultar e inserir, e é ele que a maioria dos desenhos não passa.

Confira também o código de resposta da segunda e da terceira entrega. Se voltaram como erro, o provedor vai continuar reenviando: a integração está certa no dado e presa num laço que ninguém pediu.

Isso é o que fazemos em integração de sistemas

ERP, e-commerce, CRM, banco e planilha falando entre si por API, webhook ou arquivo. Sem duplicar pedido e sem uma pessoa no meio copiando dado.

Resposta em até 1 dia útil