Investigation & fixes (failover committed-job loss): - root-cause bug-851 on an owned 3-node cluster: chaotic failover causes silent committed-prefix divergence (WAL hdr.term always 0; two leaders per term; no (term,lsn) reconcile). Evidence in repro-851/ + INVESTIGATION. - live-validated fixes 848/849/850 (recovery-on-promotion, advertise real applied_lsn, persist-layer contiguity guard); add waldump WAL inspector. Specs (strict raft.tla conformance — library owns the log): - TLA-gap-analysis.md: impl vs raft/raft.tla, gaps G1-G6. - raft-log-replication, raft-commit-safety, raft-sim-conformance: extend src/lib/raft to own log/commit/verification via callbacks (specs generated, cross-spec review passed). - ha-term-safety: controller-side adapter + config marker (R1-R7); design done. - config-upgrade-snapshot: R8 descoped here (deps entity-bearing-snapshots, bug-857). - roadmap + buglog (851/854/857) + .wolf knowledge updated.
5.1 KiB
Brief: raft-log-replication
Problem
src/lib/raft/raft.{c,h} is an election-only Raft library (terms, votes, leader election, PreVote) with a clean callback interface and a deterministic simulator (raft_sim.c). It has no log, no AppendEntries, no commit. The replicated log is hand-rolled in the controller (persist.c WAL, repl.c catch-up streaming, ha.c, ha_convergence.c) and violates the Raft log invariants of raft/raft.tla, which root-caused bug-851 (silent committed-prefix divergence under chaotic failover). Authoritative gap analysis: .kiro/specs/ha-term-safety/TLA-gap-analysis.md.
This spec closes the four log-half gaps by making the library own the log, strictly following raft.tla:
- G1 — per-entry term is dead (WAL
hdr.termalways 0;p->termnever set fromha->term). - G2 — replication matches LSN contiguity only; no
prevLogTermcheck (TLA+AppendEntries/HandleAppendEntriesRequestlogOk). - G3 — no
(term,lsn)conflict truncation (TLA+ conflict branch: delete the divergent entry + all after). - G5 — a node adopts a higher term only on vote RPCs; the AppendEntries/heartbeat path never advances
currentTerm(TLA+UpdateTermfires on any higher-term RPC). Confirmed live: a rejoined standby reportedterm:2under a term-3 primary → term reuse → two leaders per term.
Approach
Extend the library to own the replicated log, strictly mirroring the raft.tla actions, keeping the core pure / synchronous / deterministic (no I/O, no threads) so it stays simulator-verifiable. All side effects go through new callbacks the controller adapter implements over the existing WAL/repl transport.
TLA+ actions implemented (raft.tla line refs)
ClientRequest(i,v)(246) →raft_log_append(...): append[term ↦ currentTerm, value].AppendEntries(i,j)(204) → emit viaon_send_append_entrieswithprevLogIndex,prevLogTerm, ≤1+ entries,leaderCommit. Per-peernextIndex/matchIndex(TLA+ leaderVars).HandleAppendEntriesRequest(i,j,m)(327) →raft_on_append_entries_request(...):logOk=prevLogIndex==0 ∨ log[prevLogIndex].term==prevLogTerm; reject (stale term /¬logOk), candidate→follower on equal term, accept (already-have / conflict-truncate-one / append).HandleAppendEntriesResponse(i,j,m)(393) →raft_on_append_entries_reply(...): success → advancenextIndex/matchIndex; fail → decrementnextIndex.UpdateTerm(i,j,m)(406) → applied on every incoming RPC including AppendEntries/heartbeat (closes G5): higher term ⇒ adopt term, clear vote, step down to follower.
Authoritative shared contract (downstream specs MUST reference this verbatim)
Log index model: the library log index is 1-based (TLA+) and maps 1:1 to the controller WAL LSN. Per-entry term is uint64_t. New REQUIRED callbacks (pure; controller implements I/O):
int on_persist_entry(void *ctx, uint64_t index, uint64_t term, const void *entry, size_t len)— durably append/overwrite at index.uint64_t on_get_entry_term(void *ctx, uint64_t index)— term of local entry at index (0 if absent).int on_send_append_entries(void *ctx, int peer_idx, uint64_t prev_log_index, uint64_t prev_log_term, uint64_t leader_commit, /* entries handle */ ...)— transmit.int on_truncate_from(void *ctx, uint64_t index)— delete local entries atindex..end. New library entry points:raft_log_append,raft_on_append_entries_request,raft_on_append_entries_reply. Existing election callbacks/behaviour unchanged.
Scope
- In: library log model with per-entry term;
ClientRequestappend; AppendEntries send (per-peer next/match index);HandleAppendEntriesRequestaccept/reject/conflict-truncate/append by(term,index);UpdateTermon every RPC; controller adapter that maps callbacks onto the existing WAL/repl transport (WALhdr.termstamped fromha->term, carried in the repl envelope, read back on recovery); simulator coverage asserting Log Matching. - Out:
AdvanceCommitIndex/ commit-index safety +(term,lsn)vote freshness →raft-commit-safety. The full conformance harness →raft-sim-conformance. Config-change markers + upgrade snapshot →ha-term-safety. Async I/O, fan-out scheduling, the sync commit barrier, snapshot transport → controller callback adapters (not the pure core).
Preserve as policy (do NOT canonicalize away)
PreVote, priority leadership, persist-before-ACK durability + synchronous commit barrier (consumed in raft-commit-safety), "controller never self-terminates" (library returns statuses; controller owns liveness/fencing).
Verification
Extend raft_sim.c: drive AppendEntries across partitions, message drop/dup/reorder, and restarts; assert the Log Matching invariant (same (index,term) ⇒ identical prefix) and that a follower truncates a divergent suffix. Replace the controller's LSN-only reconcile with (term,index) matching delegated to the library.
Dependencies
raft-library (election core, implemented [x]). Feeds raft-commit-safety, raft-sim-conformance, and ha-term-safety integration. Closes bug-851 G1/G2/G3/G5.