Pular para conteúdo

0023 — Apagão de webhook: watchdog comercial e resgate da 1ª captura já paga

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-21 · Decidido em: 2026-09-21

Contexto

Em 18/09/2026 (sexta) quatro pedidos pagos de manhã não chegaram ao X-Adm: 260082900, 260083184, 260083199, 260083202. O cliente percebeu na segunda e importou um à mão.

A reconstrução no banco e no log do container fecha a cadeia:

hora (BRT) evento fonte
07:59:26 último webhook recebido log WebhookController
08:02:58 Falha de rede/timeout (1/3): Read Timeout log IntegradorClient
08:03:03 Connect Error: int.maxsul.xadm.biz: Temporary failure in name resolution log
08:08:13 última falha de rede — a degradação durou ~5 min log
09:12 – 10:37 os quatro pedidos viram order (orderCreated) e são pagos pied_rest
10:43:44 webhook volta — 2h44 de silêncio log
11:01:49 poll REST de 6h captura os quatro — já received pied_rest

A rede do servidor oscilou por cinco minutos; a PIED tomou timeout ao entregar e desativou o webhook. O apagão durou 2h44 — 33× a falha de rede — e só terminou quando alguém recadastrou o webhook no dashboard da PIED com a mesma URL. É o modo de falha da decisão 0016, disparado por timeout e não por 404.

O app não teve culpa nem participação: container StartedAt=2026-09-17T19:46:36Z, restarts=0, e o NormalizadorService logando de cinco em cinco minutos durante todo o silêncio. Estava ouvindo; não chegou nada.

Dois pontos cegos deixaram o estrago passar:

  1. O watchdog não viu. Ele media dia útil inteiro (24h úteis, seg–sex). Um apagão de 2h44 dentro do expediente nem se aproxima do limite — e é exatamente a janela em que o prejuízo acontece, porque é quando pedidos nascem.
  2. O backstop resgatou o dado e não conseguiu entregá-lo. O poll REST existe para cobrir buraco de webhook e fez o seu papel: trouxe os quatro. Mas o gate de envio é a transição de pagamento para received, e quem só vê o pedido depois de pago não tem transição a testemunhar. Os quatro encalharam em CAPTURADO, invisíveis. O fallback e o gate se anulavam justamente no caso que o fallback existia para cobrir.

O ponto 2 já estava registrado como trade-off conhecido ("o furo da 1ª captura", pendencias.md P1) com o console como válvula manual. O apagão mostrou que a válvula manual não basta: ninguém olha o console sem saber que há o que olhar, e o ponto 1 garantia que ninguém soubesse.

Decisão

Fechar os dois pontos cegos. As duas metades são complementares — a primeira avisa, a segunda resgata sozinha — e nenhuma substitui a outra.

A — Watchdog com régua comercial

O WatchdogJob passa a alertar por duas réguas, a que vier primeiro:

  • comercial — minutos de silêncio dentro do expediente da Maxsul (seg–sex, 8h–18h, informado pelo cliente), limite 45 min (PIED_WATCHDOG_LIMITE_MIN_COMERCIAL);
  • dia útil — as 24h úteis de hoje (PIED_WATCHDOG_LIMITE_HORAS), mantida como rede do apagão que começa fora do horário e atravessa a noite ou o fim de semana.

O intervalo do job cai de 1h para 5min: varrer de hora em hora anularia uma régua medida em dezenas de minutos. O alerta segue 1x por episódio e as duas réguas dividem o mesmo episódio — a que estourar primeiro alerta, a outra se cala.

Os 45 min são medidos, não chutados. Em 6.935 intervalos entre webhooks dentro do comercial (set/2026), apenas dois passaram de 15 min: o apagão de 26h de 14–15/09 (falha real, que a régua antiga pegou) e um gap de 37 min no almoço de 16/09. Nenhum outro acima de 20 min. 45 não gera falso positivo e teria alertado o apagão de 18/09 por volta das 08:45 — antes de o primeiro dos quatro pedidos existir (09:12).

A mensagem do alerta passa a citar o timeout ao lado do 404 como causa provável, e a dizer que a PIED não reenvia o que se perdeu — o recadastro restabelece o fluxo, não recupera o buraco.

B — Resgate da 1ª captura já paga

Quando o pedido é visto pago já na primeira captura, o upsert passa a poder enfileirá-lo direto (NA_FILA), sem transição testemunhada. O discriminador é o orderCreated da PIED, que o REST traz (o webhook não): apagão produz pedido de horas atrás; backlog antigo, de meses.

A regra vive em GatePrimeiraCaptura (pura, testável) e é levada ao SQL como um booleano — o upsert continua sendo quem sabe se houve transição. Vale tanto no INSERT quanto no DO UPDATE de um CAPTURADO que nunca transicionou (pago_em IS NULL), porque o pedido pode ter entrado antes pelo webhook (sem orderCreated) e só ganhar o campo numa passagem posterior do REST.

Duas cercas impedem despejo de backlog:

  • piso (PIED_GATE_PRIMEIRA_CAPTURA_DESDE, AAAA-MM-DD): só pedido criado depois da data de ativação. Vazio desliga o resgate, e vazio é o default — ligar é decisão de deploy. Sem o piso, subir esta versão enviaria na primeira rodada quatro pedidos legados (260081247, 260081395, 260081535, 260082852) que já andaram no fluxo e podem ter sido digitados no ZIM à mão; como o X-Adm só faz INSERT, isso viraria pedido duplicado.
  • janela (PIED_GATE_PRIMEIRA_CAPTURA_JANELA_DIAS, default 7): idade máxima do orderCreated. Cobre o apagão que atravessa um fim de semana e nunca alcança o backlog da PIED (21 mil pedidos CAPTURADO, alguns de 2025).

O Enfileirar manual do console continua existindo para o que ficar fora das cercas.

Consequências

  • O apagão vira visível em menos de uma hora, dentro do expediente, com a ação no próprio alerta. Fora do expediente nada muda — ninguém agiria antes das 8h de qualquer forma.
  • O backstop volta a ser um backstop de verdade: o poll REST que resgata o pedido perdido agora consegue entregá-lo. O par apagão-curto + poll-de-6h deixa de produzir pedido encalhado.
  • O deploy é inerte por default. Sem PIED_GATE_PRIMEIRA_CAPTURA_DESDE, B não muda nada; o comportamento é idêntico ao de antes. Ligar exige a data explícita.
  • O gate segue sendo a transição no caso normal. B não relaxa o gate: é uma exceção cercada para quem nunca teve transição a ser vista.
  • Pedido sem orderCreated no payload nunca é resgatado (falha fechada) — cai no console, como hoje.
  • O watchdog roda 12× mais vezes; o custo é uma consulta de max(recebido_em) por rodada, sob o mesmo lock de cluster (0014).

Alternativas consideradas

  • Limite comercial de 30 min: rejeitado — o gap de 37 min do almoço de 16/09 viraria alerta, e um watchdog que cria ruído semanal é um watchdog que o operador aprende a ignorar. 45 dá margem e ainda pega o apagão bem antes do primeiro pedido.
  • Trocar a régua de 24h úteis pela comercial: rejeitado — apagão iniciado às 18:01 de uma sexta ficaria sem nenhuma régua até segunda de manhã. Duas redes, custo quase zero.
  • B sem piso, só com a janela de 7 dias: rejeitado — enviaria os quatro legados no primeiro ciclo, com risco de duplicata no ZIM que ninguém pediu para correr. O piso torna o deploy inerte.
  • Janela de 2 dias em vez de 7: rejeitado — hoje pegaria zero legados, mas não cobre o apagão que começa sexta à tarde e só é notado na segunda, que é a forma mais provável do próximo incidente.
  • Enfileirar todo CAPTURADO + received: rejeitado pelo mesmo motivo de sempre (decisão de 2026-08-07) — despeja backlog antigo da PIED no ERP.
  • Detectar o apagão pelo lado da PIED (consultar se o webhook está ativo): fora de alcance, não há endpoint documentado; o cadastro é manual no dashboard (runbook).