myxce/.kiro/specs/raft-commit-safety/tasks.md
Rongsong Shen 56485e2fa3 feat(raft-commit-safety): (lastLogTerm,lastLogIndex) vote+prevote freshness (tasks 1.3, 4.1-4.2, 5.1-5.2, 6.2)
logOk (raft.tla:285, closes G6): local_last_log_term = log_len>0 ? on_get_entry_term(log_len):0;
logOk = cand_last_log_term > local || (== && cand_last_log_index >= log_len). Lexicographic:
term dominates, tie decided by index. Shared raft_log_ok helper.

Vote (4.2): logOk deny inserted after stale-term/UpdateTerm/role-gate/one-vote-per-term and
BEFORE on_vote_eligible (denies even when the app gate would grant); persist-before-grant intact.
PreVote (5.2): identical logOk, fully side-effect-free (no term/vote/role/commit mutation, no
on_persist). Signatures extended (1.3) on raft_on_vote_request/pre_vote_request; all 39 lib call
sites + sim routing updated; sim captures the candidate's send-time last-log tuple (raft_sim_rpc_t).
Empty-log call sites pass (0,0) → logOk true → existing outcomes preserved. 9 freshness tests.

NOTE: controller cluster.c vote callers still use the old signatures and will NOT compile — an
accepted, design-documented revalidation trigger; the controller adapter plumbing is owned by the
downstream ha-term-safety spec (to be implemented next). Library suite ASAN green (239 tests).
2026-06-22 00:14:26 +00:00

12 KiB

Implementation Plan

TDD-mandatory (testing.md): every implementation sub-task is preceded by a RED test in src/lib/testsuite/test_raft.c (and the simulator backing in raft_sim.c) that fails before the raft.c change and passes after. Build/run library tests via /xce-build lib.

Spec boundary (applies to ALL tasks): this spec owns the commit-index advance (raft.tla AdvanceCommitIndex + the current-term Figure-8 rule, closes G4) and the (lastLogTerm, lastLogIndex) vote/PreVote freshness (raft.tla HandleRequestVoteRequest logOk, closes G6) in src/lib/raft only. It does NOT own the log model / AppendEntries / per-peer match_index[] (those come from raft-log-replication and are reused verbatim), the synchronous commit-barrier / durability mechanism (controller — only TRIGGERED via on_commit_advanced), config markers / upgrade snapshot (ha-term-safety), or the conformance harness (raft-sim-conformance). No task here edits any controller file; the controller adapter is described, not built.

Depends (HARD): raft-log-replication — needs log_len, match_index[], and on_get_entry_term already present in raft.{c,h} and the in-memory per-node log + AppendEntries routing already in raft_sim.c.

1. Header contract: commit_index, on_commit_advanced, raft_advance_commit, freshness signatures

  • 1.1 Extend raft.h with the commit-index state and restore field

    • Add uint64_t commit_index; to raft_state_t
    • Add uint64_t initial_commit_index; to raft_config_t (restored committed LSN at boot, default 0; library never reads storage — consistent with initial_term/initial_log_len)
    • Observable completion: raft.h compiles; sizeof(raft_state_t) grows by the new field; no entry-byte storage added
    • Requirements: 1.1, 1.5
    • Boundary: raft.h
  • 1.2 Add the on_commit_advanced REQUIRED callback to raft_callbacks_t (verbatim contract)

    • Declare void on_commit_advanced(void *ctx, uint64_t commit_index) with the exact signature from design.md / brief
    • Document it as REQUIRED, fired only on strict advance, and as a TRIGGER for the existing commit barrier (never performs durability itself)
    • Observable completion: the function-pointer member exists in raft_callbacks_t with the verbatim signature; header doc comment matches the Authoritative shared contract
    • Requirements: 2.1, 2.4, 5.1
    • Boundary: raft.h
    • Depends: 1.1
  • 1.3 Declare raft_advance_commit and extend the vote/PreVote signatures with last_log_term/last_log_index

    • Declare int raft_advance_commit(raft_state_t *s)
    • Extend raft_on_vote_request and raft_on_pre_vote_request to accept cand_last_log_term and cand_last_log_index (the chosen "library owns logOk" plumbing — Option B in design.md)
    • Update the header doc comments to describe the new logOk gate and the commit-advance action
    • Observable completion: header declares raft_advance_commit and the extended vote/PreVote signatures; a stub TU including raft.h compiles
    • Requirements: 1.2, 2.2, 3.2, 4.2
    • Boundary: raft.h
    • Depends: 1.2

2. Init validation and commit-index restore

  • 2.1 RED+GREEN: raft_init requires on_commit_advanced and restores initial_commit_index
    • Test: raft_init returns -EINVAL when on_commit_advanced is NULL; returns 0 when present; s->commit_index == cfg->initial_commit_index after init
    • Implement the NULL check in raft_init (mirroring the existing required-callback checks); restore s->commit_index = cfg->initial_commit_index
    • Observable completion: new -EINVAL test case passes; restore assertion passes; existing election + log-replication init tests still green
    • Requirements: 2.5, 1.1
    • Boundary: raft.c, test_raft.c
    • Depends: 1.3

3. AdvanceCommitIndex with the current-term Figure-8 rule (raft.tla:259/270, closes G4)

  • 3.1 RED: commit-advance tests (current-term advance, Figure-8 hold, monotonic, non-leader)

    • Test (advance): leader with match_index[] quorum-agreeing through index k, entry at k from currentTerm → commit_index advances to k, on_commit_advanced(k) fired once
    • Test (Figure-8 hold): quorum agrees through a previous-term entry at k with no current-term entry above → commit_index unchanged, on_commit_advanced NOT fired
    • Test (empty agreeIndexes): no index reaches quorum → unchanged, no callback
    • Test (monotonic): a later call computing a smaller Max(agreeIndexes) never lowers commit_index
    • Test (non-leader): raft_advance_commit returns -EPERM, no change, no callback
    • Observable completion: tests compile and FAIL (no implementation yet)
    • Requirements: 1.2, 1.3, 1.4, 1.5, 1.6, 2.2, 2.3
    • Boundary: test_raft.c
    • Depends: 2.1
  • 3.2 GREEN: implement raft_advance_commit

    • Leader-only guard (-EPERM otherwise); compute Max(agreeIndexes) by scanning candidate indexes from log_len downward, seeding the agree-count with self, until a quorum ((n_peers/2)+1) is reached
    • Apply the current-term rule: advance to Max(agreeIndexes) only when on_get_entry_term(Max) == s->term; else hold
    • Monotonic: only ever move commit_index forward; fire on_commit_advanced(commit_index) exactly once on a strict advance; read entry terms only via on_get_entry_term
    • Observable completion: 3.1 tests pass; the current-term check is unmistakably present before the advance in raft.c; commit_index <= log_len holds
    • Requirements: 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 2.2, 2.3
    • Boundary: raft.c
    • Depends: 3.1
  • 3.3 RED+GREEN: commit-advance is a separate action from append-entries reply

    • Test: a raft_on_append_entries_reply that updates match_index[] to a committable quorum does NOT by itself change commit_index or fire on_commit_advanced; only a subsequent raft_advance_commit does
    • Verify (no implementation change expected in raft_on_append_entries_reply — it is owned upstream and must stay untouched); if a regression is found, the fix belongs upstream, not here
    • Observable completion: test passes; raft_on_append_entries_reply remains free of any commit-advance side effect
    • Requirements: 2.6
    • Boundary: test_raft.c
    • Depends: 3.2

4. (lastLogTerm, lastLogIndex) vote freshness (raft.tla:285, closes G6)

  • 4.1 RED: binding-vote freshness tests

    • Test: candidate with stale last_log_term denied even when last_log_index is greater; candidate with fresher last_log_term granted with a smaller last_log_index; tie on term grants only when cand_last_log_index >= log_len; the voter's own last-log term/index come from on_get_entry_term(log_len)/log_len (term 0 when empty)
    • Test: !logOk denies even when on_vote_eligible would grant (logOk applied BEFORE the application gate); persist-before-grant unchanged on the grant path
    • Observable completion: tests compile and FAIL
    • Requirements: 3.1, 3.3, 3.4, 3.5, 3.6
    • Boundary: test_raft.c
    • Depends: 1.3
  • 4.2 GREEN: implement the logOk gate in raft_on_vote_request

    • Compute local_last_log_term = (log_len > 0) ? on_get_entry_term(log_len) : 0; logOk = (cand_last_log_term > local_last_log_term) || (cand_last_log_term == local_last_log_term && cand_last_log_index >= log_len)
    • Insert the logOk deny BEFORE on_vote_eligible, after the existing stale-term/UpdateTerm/one-vote-per-term steps; leave persist-before-grant and all existing invariants in place
    • Observable completion: 4.1 tests pass; no regression in existing vote tests; the logOk block is clearly before on_vote_eligible in raft.c
    • Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6
    • Boundary: raft.c
    • Depends: 4.1

5. (lastLogTerm, lastLogIndex) PreVote freshness (raft.tla:285, side-effect-free)

  • 5.1 RED: PreVote freshness tests (identical rule, no side effects)

    • Test: raft_on_pre_vote_request applies the SAME (lastLogTerm, lastLogIndex) logOk as the binding vote; a stale tuple is denied; the existing stale-term deny and on_vote_eligible gate remain in force
    • Test: across grant and deny, no mutation of term/voted_for/voted_for_term/role/commit_index and no on_persist call (PreVote invariant)
    • Observable completion: tests compile and FAIL
    • Requirements: 4.1, 4.2, 4.3, 4.4, 4.5
    • Boundary: test_raft.c
    • Depends: 1.3
  • 5.2 GREEN: implement the logOk gate in raft_on_pre_vote_request

    • Apply the identical logOk predicate (factored to match 4.2) before/with the existing eligibility gate; mutate nothing; no on_persist
    • Observable completion: 5.1 tests pass; no regression in existing PreVote tests; the predicate matches the binding-vote path exactly
    • Requirements: 4.1, 4.2, 4.3, 4.4, 4.5
    • Boundary: raft.c
    • Depends: 5.1

6. Simulator backing: commit-advance driver + freshness plumbing

  • 6.1 Wire on_commit_advanced and a commit-advance driver in raft_sim.c

    • Add a per-raft_sim_node_t commit_index mirror updated from on_commit_advanced; implement the callback (record the value + count fires for assertions)
    • Add a sim helper to drive raft_advance_commit on a given node (or all leaders) after delivery
    • Pass each node's restored initial_commit_index (default 0) in raft_sim_init; wire on_commit_advanced for every node
    • Observable completion: after replication + delivery, driving the helper advances the leader's mirrored commit_index; the callback fire-count is observable
    • Requirements: 6.1, 6.6, 2.5
    • Boundary: raft_sim.c
    • Depends: 3.2
  • 6.2 Plumb (last_log_term, last_log_index) through the sim vote/PreVote routing

    • Update the sim's vote and PreVote delivery to read the candidate's last-log (term, index) from its in-memory log (term at log_len / log_len) and pass them to the extended raft_on_vote_request / raft_on_pre_vote_request
    • Observable completion: existing election sim tests still pass with the extended signatures; a node with a stale last-log term is denied a vote it would previously have won on LSN alone
    • Requirements: 6.5, 3.2, 4.2
    • Boundary: raft_sim.c
    • Depends: 4.2, 5.2

7. Simulator invariant tests (Figure-8, Leader Completeness, freshness, trigger semantics)

  • 7.1 Figure-8 commit-refusal test (G4)

    • Test: construct a previous-term entry replicated to a quorum but not covered by a committed current-term entry; drive raft_advance_commit and assert commit_index does NOT move over it; then replicate a current-term entry above it to a quorum, drive raft_advance_commit, and assert commit_index advances over BOTH (indirect commit)
    • Observable completion: test passes end-to-end via the sim; the held phase and the released phase are both asserted
    • Requirements: 6.2, 6.3
    • Boundary: test_raft.c, raft_sim.c
    • Depends: 6.1
  • 7.2 Leader Completeness across forced leader changes

    • Test: drive replication to a commit, then force leader changes (timeout / partition); assert any entry committed (≤ commit_index on the committing leader) is present in the log of every node that subsequently becomes leader
    • Observable completion: test passes; no future leader is missing a committed entry across the churn
    • Requirements: 6.4
    • Boundary: test_raft.c, raft_sim.c
    • Depends: 6.2, 7.1
  • 7.3 Freshness-gate lexicographic exercise

    • Test: drive votes with mismatched (last_log_term, last_log_index) tuples and assert the lexicographic outcome — stale-term-but-higher-index denied; fresher-term-but-lower-index granted; tie-term decided by index
    • Observable completion: test passes for both vote and PreVote paths
    • Requirements: 6.5
    • Boundary: test_raft.c, raft_sim.c
    • Depends: 6.2
  • 7.4 Trigger-semantics test for on_commit_advanced

    • Test: on_commit_advanced fires exactly once on a strict advance and does NOT fire while the current-term rule holds the commit index (Figure-8 held phase) or when not leader
    • Observable completion: test passes; the callback fire-count matches the number of strict advances
    • Requirements: 6.6, 2.2, 2.3
    • Boundary: test_raft.c, raft_sim.c
    • Depends: 6.1, 7.1

8. Verification

  • 8.1 Full library suite green under ASAN
    • Run /xce-build lib; all existing election + log-replication tests plus the new commit-safety and freshness tests pass under ASAN with zero leaks
    • Observable completion: test binary exits 0; ASAN clean; no regression in the election-core or log-replication tests
    • Requirements: 1.2, 1.3, 2.2, 3.1, 4.1, 6.2, 6.4
    • Boundary: src/lib/raft, src/lib/testsuite
    • Depends: 7.1, 7.2, 7.3, 7.4