Rede de segurança do failover no modo :horde (v0.3).
Por que existe. O failover automático do Horde sofre de uma corrida sob
queda abrupta do nó dono: o Horde.NodeListener (members: :auto) reage ao
:nodedown removendo o membro morto do CRDT sem realocar seus processos,
enquanto o caminho via monitor ({:DOWN} → marca o membro :dead) é o único
que dispara o handoff_processes. Quando o NodeListener ganha a corrida, o
grafo órfão nunca é reiniciado e fica perdido até intervenção manual
(observado ~50-60% das vezes). Veja docs/distribution.md.
O que este processo faz. Roda em cada nó (é o que o app adiciona à
árvore via {MeliGraph, distribution: :horde, ...}). A cada reconcile_interval
ele pergunta ao Horde.Registry "existe um dono vivo para este grafo?"
(lookup_owner/1, leitura local do CRDT, ~µs). Se sim, não faz nada. Se o grafo
está sem dono por mais de reconcile_grace, re-chama MeliGraph.Distributed.ensure_started/1,
que pede ao Horde para colocar uma árvore nova e vazia num nó vivo; o
Bootstrapper dessa árvore roda o on_ready e repovoa a ETS a partir da fonte
da verdade (Postgres). O grafo nunca é transferido entre nós — é
reconstruído.
Por que o guard é lookup_owner e não a idempotência por id. O handoff do
Horde reinicia com randomize_child_id, então após uma recuperação dirigida
pelo Horde o id do filho deixa de ser o nosso {MeliGraph.Supervisor, name}
estável — start_child não o veria como já-iniciado e subiria uma 2ª árvore
(dupla-carga → bug de soma de pesos da v0.2.x). O ConfigHolder registra sob o
conf.name estável no HordeRegistry, então lookup_owner enxerga
qualquer árvore viva (id estável OU randomizado) e nos mantém fora do caminho
quando o Horde já recuperou. O reconcile_grace (> recuperação normal do Horde)
fecha a janela de sobreposição.
Reaper de duplicata (corrida de boot/split). O start_child do
Horde.DynamicSupervisor é a parte fraca/eventual: sob boot simultâneo de
2-3 nós (cada um chamando ensure_started/1 antes do CRDT convergir), mais de
um nó pode subir a árvore localmente → grafos zumbis duplicados (e o bug de
soma de pesos da v0.2.x). O Horde.Registry (:unique), por outro lado, é um
árbitro forte: converge para UM dono de forma confiável. Logo, a cada tick,
se lookup_owner/1 aponta o dono num outro nó vivo e eu tenho uma árvore
local viva (Process.whereis(MeliGraph.Supervisor.local_name(name))), então
EU sou o zumbi (perdi a eleição do Registry) → reapo minha árvore local
(Distributed.reap_local/1). Isso não luta com timing: deixa o Registry decidir
o vencedor (sempre converge) e só varre os perdedores — em 1-2 ticks todo
duplicado de boot morre, independentemente de como surgiu. Emite
[:meli_graph, :reconciler, :reap] quando atua.