Pular para conteúdo

0035 — Eleição de instância no relay M2M

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

Complementa a 0021, que fixa a entrega at-least-once e o filtro da SAIDA por tipo.

Contexto

O filtro por tipo da 0021 particiona a fila entre apps diferentes. Ele não separa réplicas do mesmo app: elas registram os mesmos tipo, casam o mesmo predicado e drenam a mesma linha. E duas instâncias do mesmo app coexistem na casa por motivos normais — sobreposição durante o deploy, réplica, religamento do fallback JVM. Nessa janela o efeito externo (o PUT no destino) sai em dobro, não por corrida rara, mas toda vez que as duas estão no ar.

Decisão

O tick do relay roda sob advisory lock do Postgres chaveado pelo nome do app (pg_try_advisory_lock(hashtext('<micronaut.application.name>_relay_m2m'))). A réplica que não adquire o lock desiste do tick — não roda, não enfileira — e o próximo tick reencaixa.

  • A chave é por app, não global à tabela. Dois apps que dividem a mesma mensagem_m2m drenam tipo disjuntos e não têm por que bloquear um ao outro. O lock compõe com o filtro por tipo, não o substitui.
  • O lock é por sessão (pg_try_advisory_lock), não por transação: o relay seleciona fora de transação e despacha cada mensagem na transação curta dela, então um lock transacional soltaria antes do envio, que é justamente o trecho a proteger.
  • try, não bloqueante: a réplica perdedora desiste em vez de enfileirar trabalho duplicado.
  • Duplicação evitável × inerente. Evitável é duas réplicas fazendo o mesmo trabalho ao mesmo tempo — a lib tem como não emitir. Inerente é o crash entre o efeito externo e o commit — o destino já viu a chamada e o remetente não sabe, e a reentrega é a única saída correta. Remover a evitável não é exactly-once: a garantia segue at-least-once e a idempotência do receptor por chave natural segue precondição.

Alternativas descartadas

  • Reivindicar a linha da fila (FOR UPDATE SKIP LOCKED ou claim persistido). O lock de linha seria solto antes do PUT, pelo mesmo motivo do lock transacional. Reivindicar de verdade exigiria estado persistido (em voo, marca de reivindicação, reaper de órfão), que é o estado que a 0021 descartou ao deferir exactly-once.
  • Chave global à tabela. Faria um app coalescer por causa do outro, sem ganho.
  • Desligar o relay por config em uma das réplicas. Não compõe com deploy, réplica nem fallback, e some no dia em que alguém esquece a env.

Consequências

  • Sob N réplicas, a adoção não pede nada do app: sem DDL, sem coluna, sem config obrigatória; o nome do lock deriva do micronaut.application.name. A escotilha mensageria.relay.cluster-lock (default true) desliga a guarda sem release da lib, e desligar devolve a duplicação evitável.
  • A mecânica geral — qualquer job agendado sob N réplicas elege instância — mora em java-micronaut §Banco, com a guarda que avisa no pipeline.yml.