feat(ha-prevote): implement pure voter-side PreVote decision (task 2.1)

raft_on_pre_vote_request: deny on stale term, else reuse on_vote_eligible
(primary/heartbeat-stickiness/catch-up gates). Never mutates term/voted_for/
role and never persists or adopts a higher term. 6 RED-first raft unit tests
assert the purity invariant; libxce suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-06-16 04:24:23 +00:00
parent 449b8e9e93
commit 722fee5323
3 changed files with 249 additions and 1 deletions

View file

@ -190,6 +190,45 @@ int raft_on_vote_request(raft_state_t *s, int from_idx, uint64_t term,
return RAFT_GRANT;
}
int raft_on_pre_vote_request(raft_state_t *s, int from_idx, uint64_t cand_term)
{
/* PreVote is a side-effect-free straw poll: it answers "would you vote for
* me in cand_term?" WITHOUT changing any persistent state. Unlike
* raft_on_vote_request, this function never adopts cand_term (even when
* higher — that adoption is the very thing PreVote exists to prevent), never
* touches term / voted_for / voted_for_term / role, and never calls
* on_persist. The dispatcher applies the freshness floor before this call,
* exactly as on the real-vote path. */
/* Step 1: stale term — deny (mirrors raft_on_vote_request Step 1). A
* candidate proposing a term we already passed could never win a real
* election, so it cannot win a PreVote either. No term adoption: even when
* cand_term > s->term we only evaluate, never adopt. */
if (cand_term < s->term) {
if (s->cbs->on_trace)
s->cbs->on_trace(s->ctx, "pre_vote_denied_stale_term", NULL);
return RAFT_DENY;
}
/* Step 2: application-level eligibility gate — reuse the EXISTING
* on_vote_eligible callback (primary-deny / fresh-heartbeat stickiness /
* catch-up deny), identical to raft_on_vote_request Step 5. If NULL, every
* Raft-eligible candidate is granted. */
if (s->cbs->on_vote_eligible != NULL) {
if (s->cbs->on_vote_eligible(s->ctx, from_idx, cand_term) == 0) {
if (s->cbs->on_trace)
s->cbs->on_trace(s->ctx, "pre_vote_denied_eligibility", NULL);
return RAFT_DENY;
}
}
/* Grant — record NOTHING. No voted_for write (so a later real vote in this
* term is still grantable), no persist, no term/role change. */
if (s->cbs->on_trace)
s->cbs->on_trace(s->ctx, "pre_vote_granted", NULL);
return RAFT_GRANT;
}
int raft_on_vote_reply(raft_state_t *s, int from_idx, int granted, uint64_t term)
{
(void)from_idx;