feat(ha-prevote): add raft-library PreVote types, state, callback seam (task 1.2)

Scaffolding only (no bodies): RAFT_PRE_VOTE_WON/LOST/PENDING outcome codes;
pre_vote_in_progress / pre_votes_granted / pre_vote_replies[] tally fields
appended to raft_state_t; optional on_send_pre_vote_request callback. Adds
RAFT_MAX_PEERS=8 to bound per-peer dedup (production cap MAX_HA_CANDIDATES=4).
Existing raft election state/layout unchanged; libxce test green.

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

View file

@ -12,6 +12,11 @@ typedef enum {
RAFT_LEADER = 2,
} raft_role_t;
/* Upper bound on cluster size for the fixed-size pre-vote reply-tracking
* array. Matches the simulator bound (RAFT_SIM_MAX_NODES); real clusters
* are far smaller. */
#define RAFT_MAX_PEERS 8
/* Forward declaration — full definition follows raft_state_t */
typedef struct raft_callbacks_t raft_callbacks_t;
@ -32,6 +37,13 @@ typedef struct {
int election_decided; /* 1 once on_become_leader/follower fired */
uint32_t election_timeout_ms;
int my_priority; /* priority index for jitter (0 = highest) */
/* ---- PreVote round tally (Req 1.2) ----
* Self-contained pre-vote bookkeeping. A pre-vote round mutates NONE of
* the persistent Raft state above (term, voted_for, role); these fields
* only track the in-flight straw poll and are reset at each round start. */
int pre_vote_in_progress; /* 1 while a pre-vote round is active */
int pre_votes_granted; /* grants collected this round (incl. self) */
int pre_vote_replies[RAFT_MAX_PEERS]; /* per-peer replied flag; avoids double-count */
/* set by raft_init — needed by the state machine to fire callbacks */
const raft_callbacks_t *cbs;
void *ctx;
@ -60,14 +72,23 @@ typedef struct {
* on_start_election_after_delay
*
* OPTIONAL callbacks (may be NULL):
* on_vote_eligible — if NULL, all Raft-eligible candidates are granted
* on_trace — if NULL, tracing is suppressed
* on_vote_eligible — if NULL, all Raft-eligible candidates are granted
* on_send_pre_vote_request — if NULL, pre-vote emits are suppressed
* on_trace — if NULL, tracing is suppressed
*/
typedef struct raft_callbacks_t {
/* REQUIRED: Send a RequestVote RPC to peer peer_idx for the given term.
* The caller encodes config_tag, log_lsn, and any other fields in the wire RPC. */
int (*on_send_vote_request)(void *ctx, int peer_idx, uint64_t term);
/* OPTIONAL: Send a PreVote (straw-poll) request to peer peer_idx for the
* prospective term (current term + 1). Unlike on_send_vote_request, this
* MUST NOT cause the peer to persist or mutate term/voted_for — it is a
* non-binding probe. If NULL, the raft library suppresses the emit and
* the caller is responsible for any pre-vote transport itself. */
int (*on_send_pre_vote_request)(void *ctx, int peer_idx,
uint64_t prospective_term);
/* OPTIONAL: Application-specific vote eligibility gate.
* Called after Raft invariants pass (correct term, voted_for unset).
* Returns 1 to grant, 0 to deny. Receives the candidate's peer_idx and
@ -136,6 +157,13 @@ typedef struct {
#define RAFT_SKIP_ELECTION 5
#define RAFT_ELECTION_STARTED 6
/* PreVote (straw-poll) outcomes returned by raft_on_pre_vote_reply.
* Distinct values from the election codes above; a pre-vote round mutates
* no persistent state, so these never imply a term/role transition. */
#define RAFT_PRE_VOTE_WON 7 /* quorum of grants reached this round */
#define RAFT_PRE_VOTE_LOST 8 /* all peers replied, no quorum */
#define RAFT_PRE_VOTE_PENDING 9 /* still awaiting replies */
/*
* raft_init: initialize raft_state_t from a raft_config_t.
* on_vote_eligible and on_trace may be NULL (optional policy / observability).
@ -189,6 +217,47 @@ void raft_on_leader_lost(raft_state_t *s);
*/
int raft_check_and_start_election(raft_state_t *s);
/* ---------- PreVote (straw poll before a binding election) ----------
*
* The pre-vote round is a non-binding probe: it asks peers whether they WOULD
* grant a vote at term+1, without anyone incrementing term, setting voted_for,
* or persisting. Only on RAFT_PRE_VOTE_WON does the caller proceed to the
* unchanged raft_start_election (which performs the real term bump + persist).
*
* INVARIANT (Req 1.2, 6.2): none of these three functions mutate s->term,
* s->voted_for, s->voted_for_term, or s->role, and none invoke on_persist.
*/
/*
* raft_begin_pre_vote: start a pre-vote round.
* Computes prospective term = term + 1 (NOT assigned to s->term), sets
* pre_vote_in_progress, pre_votes_granted = 1 (self), and fires
* on_send_pre_vote_request(ctx, peer_idx, prospective_term) for each peer
* (NULL-checked — suppressed if the callback is unset).
* Precondition: role == RAFT_FOLLOWER.
*/
int raft_begin_pre_vote(raft_state_t *s);
/*
* raft_on_pre_vote_request: answer an incoming PreVote probe.
* Denies if cand_term < s->term; otherwise returns on_vote_eligible(ctx,
* from_idx, cand_term) (granting when the gate is NULL). Never touches term,
* voted_for, role, or fires on_persist. Freshness is applied by the
* dispatcher before this call (same contract as the real vote path).
* Returns 1 = grant, 0 = deny.
*/
int raft_on_pre_vote_request(raft_state_t *s, int from_idx, uint64_t cand_term);
/*
* raft_on_pre_vote_reply: tally a reply to our PreVote.
* Owns the pre-vote tally in raft_state_t (pre_votes_granted,
* pre_vote_replies), mirroring raft_on_vote_reply but WITHOUT firing
* on_become_leader/on_become_follower or making any role/term change.
* Returns RAFT_PRE_VOTE_WON at quorum, RAFT_PRE_VOTE_LOST when all peers
* replied without quorum, else RAFT_PRE_VOTE_PENDING.
*/
int raft_on_pre_vote_reply(raft_state_t *s, int peer_idx, int granted);
/* Fills snap with current state. Caller must hold lock. */
void raft_snapshot(const raft_state_t *s, raft_snapshot_t *snap);