From b8453b2d2ea33f8821572dcb271006e540d2ae09 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Tue, 30 Jun 2026 11:16:40 +0000 Subject: [PATCH] =?UTF-8?q?fix(controller):=20advertise=20true=20last-log?= =?UTF-8?q?=20term=20in=20vote/prevote=20=E2=80=94=20unblock=20round-4=20e?= =?UTF-8?q?lection=20(bug-851/task=205.5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The election cross-deny that stalled round 4 (after bug-1282+bug-1286 fixed): the freshest survivor (e.g. ctrl-2 tip 15 @ term 5) could not win because it advertised lastLogTerm = c->persist.term (the persist layer's RUNNING term, stale/0 after a restart) instead of the term STAMPED ON its last WAL entry. So a voter (ctrl-3, last entry term 4) saw the candidate's lastLogTerm <= its own and raft_log_ok rejected the genuinely-fresher candidate -> every round timed out -> permanent stall. This was a documented stopgap (ha.c "task 1.3" NOTE) and is the bug-851 term-identity family. Fix both advertise sites (real-vote raft_cb_send_vote_request + PreVote raft_cb_send_pre_vote_request): lastLogTerm = persist_entry_term(&c->persist, ha->raft.log_len) when log_len>0 — the SAME accessor voters use to compute their own last-log term (on_get_entry_term -> persist_entry_term), so logOk is now symmetric and the freshest survivor wins. Strengthens Election Safety/Leader Completeness (only genuinely-fresher candidates are granted); the index source (ha->raft.log_len, seeded from the recovered WAL tip) is unchanged. Root-caused from a frozen round-4 soak: ctrl-2 (15@t5) vs ctrl-3 (11@t4); ctrl-3 denied ctrl-2 on logOk (mislabeled "eligibility"). Re-running 7.2 to confirm. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC --- src/controller/ha.c | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/controller/ha.c b/src/controller/ha.c index 9230b406..bff52132 100644 --- a/src/controller/ha.c +++ b/src/controller/ha.c @@ -179,12 +179,20 @@ static int raft_cb_send_vote_request(void *ctx, int peer_idx, uint64_t term) vctx->my_lsn = (ectx->n_peers > 0) ? ectx->votes[0].my_lsn : 0; /* ha-term-safety task 1.2: advertise our Raft (lastLogTerm, lastLogIndex). * lastLogIndex == our WAL log length (the library's log_len == WAL tip LSN); - * lastLogTerm == the recovered/live WAL term that stamps that last entry. - * NOTE: sourced directly from persist (c->persist.term / raft.log_len), NOT - * via on_get_entry_term — that callback seam is task 2.x and is unwired here - * (see CONCERNS). The formal persist_entry_term accessor is task 1.3. */ + * lastLogTerm == the term STAMPED ON THAT LAST ENTRY. + * + * bug-851 / task 5.5: lastLogTerm MUST be the term of the entry at log_len — + * sourced via persist_entry_term, the SAME accessor voters use to compute + * their own last-log term (on_get_entry_term → persist_entry_term, ha.c:1556). + * The old stopgap advertised c->persist.term (the persist layer's RUNNING + * term), which is stale/0 after a restart and does NOT equal the last entry's + * stored term. That made a freshly-restarted but freshest survivor advertise + * a too-low lastLogTerm, so every voter's logOk rejected it (asymmetric with + * how the voter computes its own term) and the election stalled. */ vctx->my_last_log_index = ha->raft.log_len; - vctx->my_last_log_term = c ? c->persist.term : 0; + vctx->my_last_log_term = (c && ha->raft.log_len > 0) + ? persist_entry_term(&c->persist, ha->raft.log_len) + : 0; vctx->my_name = c->myconf ? c->myconf->name : ""; /* Get a fresh endpoint */ @@ -294,10 +302,14 @@ static int raft_cb_send_pre_vote_request(void *ctx, int peer_idx, vctx->my_term = prospective_term; vctx->my_tag = (ectx->n_peers > 0) ? ectx->votes[0].my_tag : 0; vctx->my_lsn = (ectx->n_peers > 0) ? ectx->votes[0].my_lsn : 0; - /* ha-term-safety task 1.2: advertise our Raft (lastLogTerm, lastLogIndex) - * — identical sourcing to the real-vote send path above. */ + /* ha-term-safety task 1.2 + bug-851/task 5.5: advertise lastLogTerm as the + * term of the entry at log_len (persist_entry_term — the same source voters + * use), NOT the stale c->persist.term. Identical sourcing to the real-vote + * send path above; see the full rationale there. */ vctx->my_last_log_index = ha->raft.log_len; - vctx->my_last_log_term = c ? c->persist.term : 0; + vctx->my_last_log_term = (c && ha->raft.log_len > 0) + ? persist_entry_term(&c->persist, ha->raft.log_len) + : 0; vctx->my_name = c->myconf ? c->myconf->name : ""; endpoint_t *ep = endpoint_get();