Skip to content

Commit fcf9719

Browse files
claudehyperpolymath
authored andcommitted
fix(elixir): deterministic KRaft 3-node propose + guard telemetry entity count
The "3-node cluster persists state across all nodes" recovery test read diagnostics once, picked a node reporting :leader, then proposed to it. On a freshly-started cluster a re-election can step that node down inside the race window, so propose returned {:error, {:not_leader, _}} and the {:ok, _} match crashed (kraft_recovery_test.exs:249, the lone failure on run #90). Replace snapshot-then-propose with an await-leader barrier: poll for a node that is leader, propose to it, follow any {:not_leader, _} redirect and retry across re-elections until the cluster commits or a deadline elapses. A deterministic harness rather than a fixed sleep, per #110. Also guard VeriSim.Telemetry.measure_entity_count/0 against the entity registry not being started (test env): catch the "unknown registry" ArgumentError and report 0, so telemetry_poller no longer logs a crashed measurement on every tick. Refs #110. https://claude.ai/code/session_01W9Voe3JceP66Bna9FT4jME
1 parent de9ff4d commit fcf9719

2 files changed

Lines changed: 73 additions & 18 deletions

File tree

elixir-orchestration/lib/verisim/telemetry.ex

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,16 +197,25 @@ defmodule VeriSim.Telemetry do
197197

198198
@doc false
199199
def measure_entity_count do
200-
# Count entity servers
200+
# Count entity servers. The registry may not be running (e.g. in the test
201+
# env, where the full supervision tree isn't started), so guard against the
202+
# "unknown registry" ArgumentError rather than letting telemetry_poller log
203+
# a crashed measurement on every tick.
201204
count =
202-
case Registry.count(VeriSim.EntityRegistry) do
205+
case registered_entity_count() do
203206
n when is_integer(n) -> n
204207
_ -> 0
205208
end
206209

207210
:telemetry.execute([:verisim, :entities], %{count: count}, %{})
208211
end
209212

213+
defp registered_entity_count do
214+
Registry.count(VeriSim.EntityRegistry)
215+
rescue
216+
ArgumentError -> 0
217+
end
218+
210219
@doc false
211220
def measure_drift_status do
212221
# Get drift status from monitor

elixir-orchestration/test/verisim/consensus/kraft_recovery_test.exs

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,55 @@ defmodule VeriSim.Consensus.KRaftRecoveryTest do
3737
:exit, _ -> :ok
3838
end
3939

40+
# Propose `command` through whichever node is currently the leader, retrying
41+
# across re-elections and following {:not_leader, _} redirects, until the
42+
# cluster accepts it (-> {:ok, index}) or `timeout_ms` elapses. Deterministic
43+
# alternative to "read diagnostics once, propose once", which races with a
44+
# leader step-down on a freshly-started cluster.
45+
defp propose_when_leader(ids, command, timeout_ms \\ 5_000) do
46+
deadline = System.monotonic_time(:millisecond) + timeout_ms
47+
do_propose_when_leader(ids, command, deadline)
48+
end
49+
50+
defp do_propose_when_leader(ids, command, deadline) do
51+
leader =
52+
Enum.find_value(ids, fn id ->
53+
case safe_diagnostics(id) do
54+
%{role: :leader} -> id
55+
_ -> nil
56+
end
57+
end)
58+
59+
result = if leader, do: safe_propose(leader, command), else: {:error, :no_leader}
60+
61+
case result do
62+
{:ok, _} = ok ->
63+
ok
64+
65+
_other ->
66+
if System.monotonic_time(:millisecond) < deadline do
67+
Process.sleep(50)
68+
do_propose_when_leader(ids, command, deadline)
69+
else
70+
result
71+
end
72+
end
73+
end
74+
75+
# GenServer.call wrappers that tolerate a node being mid-transition: a call can
76+
# time out or the process can be briefly unavailable during an election.
77+
defp safe_diagnostics(id) do
78+
KRaftNode.diagnostics(id)
79+
catch
80+
:exit, _ -> %{role: :unknown}
81+
end
82+
83+
defp safe_propose(id, command) do
84+
KRaftNode.propose(id, command)
85+
catch
86+
:exit, _ -> {:error, :unavailable}
87+
end
88+
4089
# ===========================================================================
4190
# WAL persistence during normal operation
4291
# ===========================================================================
@@ -236,22 +285,19 @@ defmodule VeriSim.Consensus.KRaftRecoveryTest do
236285
start_node(id, peers: peers, wal_path: wal_path)
237286
end)
238287

239-
# Wait for election
240-
Process.sleep(1_500)
241-
242-
# Find the leader
243-
{leader_id, _} =
244-
ids
245-
|> Enum.map(fn id -> {id, KRaftNode.diagnostics(id)} end)
246-
|> Enum.find(fn {_id, diag} -> diag.role == :leader end)
247-
248-
# Propose a command through the leader
249-
{:ok, _} =
250-
KRaftNode.propose(
251-
leader_id,
252-
{:register_store, "cluster-store", "http://cluster:8080", ["graph"]}
253-
)
254-
288+
# Propose through whichever node is currently the leader, retrying across
289+
# re-elections. A freshly-started 3-node cluster can re-elect during the
290+
# first election cycles, so reading diagnostics once and proposing to that
291+
# node races with a step-down (-> {:error, {:not_leader, _}}). Await a node
292+
# that is leader *and* accepts the proposal, following any not_leader
293+
# redirect — a deterministic barrier rather than a fixed sleep.
294+
assert {:ok, _} =
295+
propose_when_leader(
296+
ids,
297+
{:register_store, "cluster-store", "http://cluster:8080", ["graph"]}
298+
)
299+
300+
# Let the commit replicate to the followers so every WAL records a term.
255301
Process.sleep(500)
256302

257303
# All nodes should have WAL data

0 commit comments

Comments
 (0)