fix(ha): correct real-vote reply index mapping for middle-priority nodes (bug-1417)
vote_reply_dispatch passed the compressed, self-excluded local votes_idx (vctx->peer_idx, set by raft_cb_send_vote_request's absolute->local remap) straight through to raft_on_vote_reply, which indexes vote_replies[] in ABSOLUTE cluster space -- the same space raft_start_election's self-mark and per-peer send loop use. For a 3-node cluster's middle node (self_idx=1), the peer at absolute index 2 compresses to votes_idx=1, colliding with vote_replies[self_idx=1] -- the slot raft_start_election already marked for the self-vote. The library's per-peer dedup guard then silently drops the genuine reply as a duplicate, so votes_granted never reaches quorum even though the peer granted. Peer 0 is unaffected (compressed index equals absolute), which is why sequential elections and prior soak runs never surfaced this -- only a middle-priority node losing its higher-priority peer and depending on its lower peer's vote hits the collision. Fixed by routing through ha_repl_ack_abs_peer_idx (already existed, already unit-tested, already used for the analogous heartbeat-ACK path) before calling raft_on_vote_reply. ha_record_vote_reply immediately above is unchanged -- it correctly wants the compressed index for ctx->votes[]. Confirmed pre_vote_reply_dispatch needs no equivalent fix: its send path already stores the absolute index. New regression test drives a real 3-node election end-to-end through the actual decode/dispatch path (not the test-hook bypass every other real-vote test in this file uses): Expected 2 Was 1 pre-fix, GREEN post-fix. Full 79-group suite: zero new failures. 10-round --chaotic HA soak: zero committed divergence, zero committed job loss.
This commit is contained in:
parent
48b4dba700
commit
d5122752fe
3 changed files with 163 additions and 2 deletions
|
|
@ -12562,5 +12562,50 @@
|
|||
],
|
||||
"occurrences": 11,
|
||||
"last_seen": "2026-07-05T07:45:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "bug-1417",
|
||||
"timestamp": "2026-07-07T09:30:00.000Z",
|
||||
"error_message": "[xce-test-ha --chaotic] FAIL round 4/10: election stall after killing ctrl-1 \u2014 ctrl-2 PreVote wins but real election times out despite ctrl-3 granting RequestVote",
|
||||
"file": "src/controller/ha.c",
|
||||
"root_cause": "vote_reply_dispatch passes local votes_idx (compressed, excludes self) directly to raft_on_vote_reply which expects ABSOLUTE peer index (includes self). For self_idx=1 (ctrl-2), ctrl-3's vote (absolute=2, local=1) lands in vote_replies[1]=ctrl-2's own self-slot \u2192 treated as duplicate \u2192 vote silently dropped. With ctrl-1 dead in chaotic mode, ctrl-2 only has self-vote (1 < quorum 2) \u2192 eternal timeout. Self_idx=0 and self_idx=2 are partially/fully immune due to index alignment luck. ha_repl_ack_abs_peer_idx() already exists (ha.c:3779) with correct mapping and passing tests \u2014 vote_reply_dispatch just doesn't call it.",
|
||||
"fix": [
|
||||
"FIXED (src/controller/ha.c vote_reply_dispatch, ~line 5233): raft_on_vote_reply now receives ha_repl_ack_abs_peer_idx(ha, 1, idx) instead of the raw compressed local idx. ha_repl_ack_abs_peer_idx (ha.c:3779, already existed + unit-tested for the heartbeat-ACK path) performs the exact inverse of raft_cb_send_vote_request send-side remap: votes_idx < self_idx ? votes_idx : votes_idx + 1. ha_record_vote_reply (immediately preceding line) is UNCHANGED -- it correctly wants the compressed idx for ctx->votes[]. Confirmed pre_vote_reply_dispatch needs NO equivalent fix: raft_cb_send_pre_vote_request (ha.c:301) already stores the ABSOLUTE peer_idx in vctx->peer_idx for the PreVote path (explicit code comment there), matching raft_on_pre_vote_reply/pre_vote_replies[] absolute-index expectation -- only the real-vote send path (raft_cb_send_vote_request, ha.c:175) stores compressed. RED->GREEN: new unit test test_bug1417_middle_index_real_vote_from_peer2_not_dropped (test_ha_vote_send.c) drives a REAL 3-node election (self_idx=1) through raft_start_election then a genuine peer-2 grant via vote_reply_msg_handler_for_test (the actual decode/dispatch path, not the xce_vote_reply_hook bypass every other real-vote test in this file uses) -- reproduced Expected 2 Was 1 pre-fix, GREEN post-fix (votes_granted=2, role=RAFT_LEADER). Verified: full 79-group controller_unit_test sweep 0 new failures; 10-round --chaotic xce-test-ha soak PASS (committed_divergence=0, committed_job_loss=0, 18 jobs). Soak surfaced an UNRELATED, pre-existing --chaotic harness bug (run-ha-soak.sh run_chaotic_rounds: DOWN variable is not cleared on a skip-this-round no-primary-to-kill path, so a subsequent round calls start_node on the same already-running node without killing it first -- 6 stacked ctrl-2 processes observed piling up over rounds 5-10) -- confirmed via git diff this script was untouched by this fix, logged separately, NOT fixed here (out of bug-1417 boundary)."
|
||||
],
|
||||
"tags": [
|
||||
"vote",
|
||||
"raft",
|
||||
"self_idx",
|
||||
"index-mapping",
|
||||
"chaotic-failover",
|
||||
"off-by-one",
|
||||
"fixed",
|
||||
"ha",
|
||||
"ha_repl_ack_abs_peer_idx",
|
||||
"election"
|
||||
],
|
||||
"related_bugs": [
|
||||
"bug-828"
|
||||
],
|
||||
"occurrences": 1,
|
||||
"last_seen": "2026-07-07T10:20:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "bug-1418",
|
||||
"timestamp": "2026-07-07T10:20:00.000Z",
|
||||
"error_message": "[xce-test-ha --chaotic] rounds 6-10 of a 10-round soak all skip (no primary to kill) after round 5 hit a slow-but-eventually-successful election; ctrl-2 restarted repeatedly without the prior instance ever being killed -- 6 stacked ctrl-2 xce_controller processes observed alive simultaneously by end of run",
|
||||
"file": ".claude/skills/xce-test-ha/run-ha-soak.sh",
|
||||
"root_cause": "run_chaotic_rounds's DOWN variable tracks the node killed THIS round so the NEXT round can restart it. On a round where primary_from_log finds no primary (log \"no primary to kill this round -- skipping\"), the loop continues without ever updating DOWN or killing anything -- but the NEXT round's \"if [ -n \"\" ]\" restart-previously-downed step still fires unconditionally on the stale DOWN value from several rounds back, calling start_node on a node that was never actually killed (or was already restarted in a prior skipped round), stacking a new xce_controller process on top of an already-running one every single skipped round. Discovered incidentally while verifying bug-1417's fix (10-round chaotic soak, unrelated to the vote-index fix itself -- confirmed via git diff that run-ha-soak.sh was untouched this session).",
|
||||
"fix": "NOT FIXED (out of scope for the bug-1417 investigation that surfaced it). Candidate fix: only treat a node as DOWN (eligible for the next round's restart) if this round genuinely killed it; on a skip-round, DOWN should stay whatever it was (already handled -- the bug is that the skip round should also skip minus the restart-if-still-actually-down check), or more robust: before start_node(\"\"), pgrep for an existing xce_controller --myname and skip the restart if one is already running.",
|
||||
"tags": [
|
||||
"xce-test-ha",
|
||||
"chaotic-mode",
|
||||
"harness-bug",
|
||||
"restart-bookkeeping",
|
||||
"process-leak"
|
||||
],
|
||||
"related_bugs": [],
|
||||
"occurrences": 1,
|
||||
"last_seen": "2026-07-07T10:20:00.000Z"
|
||||
}
|
||||
]
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue