From 449b8e9e935573899934c2c2034ff0e9d64ba4a5 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Tue, 16 Jun 2026 04:08:23 +0000 Subject: [PATCH] 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) --- .kiro/specs/ha-prevote/tasks.md | 6 ++- src/lib/raft/raft.h | 73 ++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/.kiro/specs/ha-prevote/tasks.md b/.kiro/specs/ha-prevote/tasks.md index 65ea0168..f72f0d9c 100644 --- a/.kiro/specs/ha-prevote/tasks.md +++ b/.kiro/specs/ha-prevote/tasks.md @@ -9,7 +9,7 @@ - _Requirements: 7.1_ - _Boundary: PreVote dispatch (wire schema)_ -- [ ] 1.2 (P) Add PreVote types, state, and callback seam to the raft library +- [x] 1.2 (P) Add PreVote types, state, and callback seam to the raft library - Add PreVote outcome codes (won / lost / pending), a self-contained PreVote tally on the raft state (granted count, per-peer replied tracking, in-progress flag), and an optional send-pre-vote callback in the callback table. - Header compiles with the new declarations and existing election state/layout is unchanged; no behavior added yet. - _Requirements: 1.2, 6.2_ @@ -90,3 +90,7 @@ - Observable: invariant tests pass and the existing failover/rejoin regression tests remain green with PreVote enabled. - _Requirements: 5.2, 5.3, 6.1, 6.2, 6.3, 6.4_ - _Depends: 2.1, 2.2, 2.3_ + +## Implementation Notes +- (1.1) Wire opcodes `preVoteRequest @38` / `preVoteReply @39` reuse existing `RequestVoteRequest`/`RequestVoteReply` structs and union arms (`requestvotemsg @27` / `requestvotereplymsg @14`). Generated bindings live under `src/xce.build/lib/controller.capnp.h` (build artifacts, not source-committed). +- (1.2) Raft PreVote scaffolding in `raft.h`: outcome codes `RAFT_PRE_VOTE_WON/LOST/PENDING` = 7/8/9; tally fields `pre_vote_in_progress`, `pre_votes_granted`, `pre_vote_replies[RAFT_MAX_PEERS]` appended to `raft_state_t` (after `my_priority`); optional `on_send_pre_vote_request` callback (NULL-checkable). New `#define RAFT_MAX_PEERS 8` bounds the per-peer dedup array — safe: production cap is `MAX_HA_CANDIDATES=4` (src/include/config.h:78), and it matches `RAFT_SIM_MAX_NODES=8`. Tasks 2.1/2.2/2.3 supply the function bodies and MUST keep them pure (no `term`/`voted_for`/`role` mutation, no `on_persist`). Build/test the raft layer with `cd src && nix develop --command bash tools/xce-build.sh lib`. diff --git a/src/lib/raft/raft.h b/src/lib/raft/raft.h index 11bc6a2e..e020d840 100644 --- a/src/lib/raft/raft.h +++ b/src/lib/raft/raft.h @@ -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);