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).
12 KiB
Implementation Plan
TDD-mandatory (
testing.md): every implementation sub-task is preceded by a RED test insrc/lib/testsuite/test_raft.c(and the simulator backing inraft_sim.c) that fails before theraft.cchange 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.tlaAdvanceCommitIndex+ the current-term Figure-8 rule, closes G4) and the(lastLogTerm, lastLogIndex)vote/PreVote freshness (raft.tlaHandleRequestVoteRequestlogOk, closes G6) insrc/lib/raftonly. It does NOT own the log model / AppendEntries / per-peermatch_index[](those come fromraft-log-replicationand are reused verbatim), the synchronous commit-barrier / durability mechanism (controller — only TRIGGERED viaon_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— needslog_len,match_index[], andon_get_entry_termalready present inraft.{c,h}and the in-memory per-node log + AppendEntries routing already inraft_sim.c.
1. Header contract: commit_index, on_commit_advanced, raft_advance_commit, freshness signatures
-
1.1 Extend
raft.hwith the commit-index state and restore field- Add
uint64_t commit_index;toraft_state_t - Add
uint64_t initial_commit_index;toraft_config_t(restored committed LSN at boot, default 0; library never reads storage — consistent withinitial_term/initial_log_len) - Observable completion:
raft.hcompiles;sizeof(raft_state_t)grows by the new field; no entry-byte storage added - Requirements: 1.1, 1.5
- Boundary: raft.h
- Add
-
1.2 Add the
on_commit_advancedREQUIRED callback toraft_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_twith the verbatim signature; header doc comment matches the Authoritative shared contract - Requirements: 2.1, 2.4, 5.1
- Boundary: raft.h
- Depends: 1.1
- Declare
-
1.3 Declare
raft_advance_commitand 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_requestandraft_on_pre_vote_requestto acceptcand_last_log_termandcand_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_commitand the extended vote/PreVote signatures; a stub TU includingraft.hcompiles - Requirements: 1.2, 2.2, 3.2, 4.2
- Boundary: raft.h
- Depends: 1.2
- Declare
2. Init validation and commit-index restore
- 2.1 RED+GREEN:
raft_initrequireson_commit_advancedand restoresinitial_commit_index- Test:
raft_initreturns-EINVALwhenon_commit_advancedis NULL; returns 0 when present;s->commit_index == cfg->initial_commit_indexafter init - Implement the NULL check in
raft_init(mirroring the existing required-callback checks); restores->commit_index = cfg->initial_commit_index - Observable completion: new
-EINVALtest 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
- Test:
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 indexk, entry atkfromcurrentTerm→commit_indexadvances tok,on_commit_advanced(k)fired once - Test (Figure-8 hold): quorum agrees through a previous-term entry at
kwith no current-term entry above →commit_indexunchanged,on_commit_advancedNOT fired - Test (empty agreeIndexes): no index reaches quorum → unchanged, no callback
- Test (monotonic): a later call computing a smaller
Max(agreeIndexes)never lowerscommit_index - Test (non-leader):
raft_advance_commitreturns-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
- Test (advance): leader with
-
3.2 GREEN: implement
raft_advance_commit- Leader-only guard (
-EPERMotherwise); computeMax(agreeIndexes)by scanning candidate indexes fromlog_lendownward, 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 whenon_get_entry_term(Max) == s->term; else hold - Monotonic: only ever move
commit_indexforward; fireon_commit_advanced(commit_index)exactly once on a strict advance; read entry terms only viaon_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_lenholds - Requirements: 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 2.2, 2.3
- Boundary: raft.c
- Depends: 3.1
- Leader-only guard (
-
3.3 RED+GREEN: commit-advance is a separate action from append-entries reply
- Test: a
raft_on_append_entries_replythat updatesmatch_index[]to a committable quorum does NOT by itself changecommit_indexor fireon_commit_advanced; only a subsequentraft_advance_commitdoes - 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_replyremains free of any commit-advance side effect - Requirements: 2.6
- Boundary: test_raft.c
- Depends: 3.2
- Test: a
4. (lastLogTerm, lastLogIndex) vote freshness (raft.tla:285, closes G6)
-
4.1 RED: binding-vote freshness tests
- Test: candidate with stale
last_log_termdenied even whenlast_log_indexis greater; candidate with fresherlast_log_termgranted with a smallerlast_log_index; tie on term grants only whencand_last_log_index >= log_len; the voter's own last-log term/index come fromon_get_entry_term(log_len)/log_len(term 0 when empty) - Test:
!logOkdenies even whenon_vote_eligiblewould 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
- Test: candidate with stale
-
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_eligibleinraft.c - Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6
- Boundary: raft.c
- Depends: 4.1
- Compute
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_requestapplies the SAME(lastLogTerm, lastLogIndex)logOk as the binding vote; a stale tuple is denied; the existing stale-term deny andon_vote_eligiblegate remain in force - Test: across grant and deny, no mutation of
term/voted_for/voted_for_term/role/commit_indexand noon_persistcall (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
- Test:
-
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
- Apply the identical logOk predicate (factored to match 4.2) before/with the existing eligibility gate; mutate nothing; no
6. Simulator backing: commit-advance driver + freshness plumbing
-
6.1 Wire
on_commit_advancedand a commit-advance driver inraft_sim.c- Add a per-
raft_sim_node_tcommit_indexmirror updated fromon_commit_advanced; implement the callback (record the value + count fires for assertions) - Add a sim helper to drive
raft_advance_commiton a given node (or all leaders) after delivery - Pass each node's restored
initial_commit_index(default 0) inraft_sim_init; wireon_commit_advancedfor 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
- Add a per-
-
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 extendedraft_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
- Update the sim's vote and PreVote delivery to read the candidate's last-log
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_commitand assertcommit_indexdoes NOT move over it; then replicate a current-term entry above it to a quorum, driveraft_advance_commit, and assertcommit_indexadvances 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
- Test: construct a previous-term entry replicated to a quorum but not covered by a committed current-term entry; drive
-
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_indexon 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
- Test: drive replication to a commit, then force leader changes (timeout / partition); assert any entry committed (≤
-
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
- Test: drive votes with mismatched
-
7.4 Trigger-semantics test for
on_commit_advanced- Test:
on_commit_advancedfires 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
- Test:
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
- Run