myxce/.kiro/specs/raft-sim-conformance/tasks.md

275 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Implementation Plan
> **TDD-mandatory** (`testing.md`): every checker, scheduler action, and scenario lands as
> a RED test in `src/lib/testsuite/test_raft.c` (with the `raft_sim.c` backing) that fails
> before the harness logic exists and passes after. Build/run library tests via
> `/xce-build lib`. All harness code is `XCE_TESTING`-gated.
>
> **Spec boundary** (applies to ALL tasks): this spec owns `src/lib/raft/raft_sim.c`, the
> `XCE_TESTING` sim section of `src/lib/raft/raft.h`, and the conformance tests in
> `src/lib/testsuite/test_raft.c` ONLY. It does **NOT** modify the protocol
> implementation: no task changes `raft.c` protocol logic or any protocol declaration in
> `raft.h`, and no task edits a controller file. The harness drives the published library
> entry points and consumes the `raft-log-replication` + `raft-commit-safety` callback
> contract **verbatim**.
>
> **Depends** (whole spec): `raft-log-replication` (log model + AppendEntries entry points
> + the four callbacks), `raft-commit-safety` (`raft_advance_commit`, `on_commit_advanced`,
> `(lastLogTerm, lastLogIndex)` freshness). If those signatures change, re-validate per the
> design's Revalidation Triggers.
## 1. Harness types and in-memory adapter backing
- [x] 1.1 Extend the `XCE_TESTING` sim section of `raft.h` with the harness data types
- Add `raft_sim_msg_kind_t`, `raft_sim_msg_t` (vote/AE req+rsp union fields), `raft_sim_log_entry_t {term, value}`, `raft_sim_action_t {kind, actor, msg_index, term, index}`, and the `RAFT_SIM_MAX_LOG/MSGS/STEPS` bounds
- Extend `raft_sim_node_t` with `log[]`, `log_len`, `commit_index`, `applied[]`, and a persisted-`(term, votedFor)` shadow for Restart; extend `raft_sim_cluster_t` with `bag[]`+`bag_count`, PRNG state, `seed`, and `trace[]`+`trace_len`
- Observable completion: `raft.h` compiles under `XCE_TESTING`; no protocol declaration changed; `sizeof(raft_sim_cluster_t)` grows by the new fixed arrays
- _Requirements: 1.1, 1.3, 2.5_
- _Boundary: raft.h (XCE_TESTING section only)_
- [x] 1.2 Implement the seeded PRNG and the in-memory adapter callbacks
- Add a small inlined deterministic generator (e.g. SplitMix64) seeded by `cluster.seed`; add `on_persist_entry` → `node.log[]`, `on_get_entry_term` → term|0, `on_truncate_from` → shrink `log_len`, `on_commit_advanced` → set `commit_index` + apply into `applied[]`; route `on_send_vote_request`/`on_send_append_entries` to a bag push
- Add `raft_sim_conformance_init(c, n, timeout, seed)` wiring all six callbacks + the bag
- Observable completion: a test initializes a cluster, drives one `raft_log_append` on a forced leader, and reads the entry back via the in-memory log and `on_get_entry_term`
- _Requirements: 1.1, 1.2, 2.1_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 1.1_
## 2. Message bag (mirrors raft.tla `messages`)
- [x] 2.1 RED+GREEN: bag push/deliver routes vote and AppendEntries RPCs through the library handlers
- Test: pushing a vote-req and an AE-req then delivering them invokes `raft_on_vote_request` / `raft_on_append_entries_request`, and the handler reply is pushed back as a `*_RSP` message
- Implement `raft_sim_bag_push` and `raft_sim_bag_deliver(i)` (route by `kind`, honor `node.partition`, push replies)
- Observable completion: delivered requests produce matching responses in the bag; partitioned endpoints are not delivered
- _Requirements: 2.2, 2.4_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 1.2_
- [x] 2.2 RED+GREEN: Drop / Duplicate / Reorder mutate only the bag
- Test: `raft_sim_bag_drop`/`_duplicate` change bag multiplicity without mutating any node's term/role/log; reorder delivers in a PRNG-chosen order
- Implement the three bag operations (mirroring `raft.tla:443`/`448` `UNCHANGED` of non-message vars)
- Observable completion: node snapshots are identical before/after a drop or duplicate of an undelivered message
- _Requirements: 2.5_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 2.1_
## 3. The four invariant checkers (TDD against hand-built clusters)
- [x] 3.1 RED+GREEN: Election Safety checker (P)
- Test: a cluster with two `LEADER` nodes at the same term fails; a single-leader-per-term cluster passes
- Implement `raft_sim_check_election_safety` over roles+terms read from `raft_snapshot`
- Observable completion: both directions assert correctly
- _Requirements: 3.1, 3.6_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 1.1_
- [x] 3.2 RED+GREEN: Log Matching checker (P)
- Test: two logs sharing `(idx, term)` but differing earlier fail; identical-prefix logs pass
- Implement `raft_sim_check_log_matching` (pairwise; shared `(idx, term)` ⇒ identical `1..idx`)
- Observable completion: both directions assert correctly
- _Requirements: 3.2, 3.6_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 1.1_
- [x] 3.3 RED+GREEN: Leader Completeness checker (P)
- Test: a committed `(index, term)` absent from a higher-term leader's log fails; present passes
- Implement `raft_sim_check_leader_completeness` using `commit_index` + per-node logs, tracking the highest-term leader to date
- Observable completion: both directions assert correctly
- _Requirements: 3.3, 3.6_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 1.1_
- [x] 3.4 RED+GREEN: State Machine Safety checker (P)
- Test: divergent `applied[k]` across two nodes fails; identical applied prefixes pass
- Implement `raft_sim_check_state_machine_safety` over `applied[]`
- Observable completion: both directions assert correctly
- _Requirements: 3.4, 3.6_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 1.1_
- [x] 3.5 RED+GREEN: `raft_sim_check_all` returns the first failing invariant and halts
- Test: a cluster violating two invariants reports the first; a clean cluster returns OK
- Implement `raft_sim_check_all` composing 3.1–3.4 in a fixed order
- Observable completion: first-failure semantics verified
- _Requirements: 3.5_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 3.1, 3.2, 3.3, 3.4_
## 4. Action scheduler and trace recorder
- [x] 4.1 RED+GREEN: trace recorder records every action before it is applied
- Test: after a fixed step sequence, `cluster.trace[]` holds the actions in order; `raft_sim_dump_trace` prints seed, n, and the ordered actions
- Implement `raft_sim_action_t` recording (R1.4) + `raft_sim_dump_trace` (R5.1, R5.2); quiet on success unless `RAFT_SIM_VERBOSE` (R5.4)
- Observable completion: a recorded trace round-trips to a readable dump
- _Requirements: 1.4, 5.1, 5.2, 5.4_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 1.1_
- [x] 4.2 RED+GREEN: `raft_sim_step` drives one Next-equivalent action through the library and checks invariants
- Test: stepping reaches each enabled action kind over a run; every step calls `raft_sim_check_all`; a forced violation halts with a trace dump and a `<0` return
- Implement the seeded scheduler dispatch (Restart, Timeout, RequestVote/BecomeLeader via vote delivery, ClientRequest, AppendEntries via `raft_send_append_entries`, AdvanceCommitIndex via `raft_advance_commit`, Receive via bag deliver, Duplicate/Drop) selecting only **enabled** actions, driving **only** library entry points
- Observable completion: a multi-step run touches log append, AE replication, commit advance, and message faults; invariants checked each step
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.6, 3.5_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 2.2, 3.5, 4.1_
- [x] 4.3 RED+GREEN: Restart preserves persisted `term`/`votedFor`/log
- Test: a Restart action re-inits a node while keeping `node.log[]`, recovered `log_len`, and last-persisted `(term, votedFor)`; volatile state resets per `raft.tla` `Restart`
- Implement the Restart branch (feed `initial_log_len` + restored `(term, votedFor)` into `raft_init`)
- Observable completion: a restarted node retains its log and term; no invariant violation results from the restart alone
- _Requirements: 2.1_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 4.2_
- [x] 4.4 RED+GREEN: deterministic replay from `(n, seed, max_steps)`
- Test: two runs with the same `(n, seed, max_steps)` reach identical final per-node + bag state and an identical recorded trace
- Implement `raft_sim_run(c, max_steps)` and confirm no wall-clock/address/uninitialized input leaks into choices
- Observable completion: byte-identical replay verified across two runs
- _Requirements: 1.5, 1.6, 5.3_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 4.2_
## 5. Targeted adversarial scenarios
- [x] 5.1 Partition/heal scenario (P)
- Implement `raft_sim_scenario_partition_heal`: isolate a minority during appends, heal, drain the bag; assert all four invariants throughout and after convergence
- Observable completion: scenario passes with zero violations on the correct build
- _Requirements: 4.1_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 4.2_
- [x] 5.2 Restart-churn scenario (P)
- Implement `raft_sim_scenario_restart_churn`: restart nodes mid-replication preserving persisted state; assert re-convergence with no violation
- Observable completion: scenario passes; restarted nodes converge to the leader's prefix
- _Requirements: 4.2_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 4.3_
- [x] 5.3 Drop/duplicate/reorder scenario (P)
- Implement `raft_sim_scenario_drop_dup_reorder`: perturb bag delivery order + multiplicity; assert idempotent convergence with all four invariants held
- Observable completion: scenario passes; duplicated AE is a no-op (already-have branch)
- _Requirements: 4.3_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 4.2_
- [x] 5.4 Figure-8 scenario
- Implement `raft_sim_scenario_figure8`: a previous-term entry on a quorum, a leader change; assert the entry is NOT reported committed until a current-term entry commits (Leader Completeness / State Machine Safety preserved)
- Observable completion: the previous-term entry's `commit_index` does not cover it until a current-term commit
- _Requirements: 4.4_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 4.2_
- [x] 5.5 Stale-rejoin scenario (bug-851 G5)
- Implement `raft_sim_scenario_stale_rejoin`: a term-lagging node rejoins under a higher-term leader; assert it adopts the higher term from the AE/heartbeat path and does not re-elect at a reused term (Election Safety preserved)
- Observable completion: the rejoining node's term advances via AE; no duplicate-term leader appears
- _Requirements: 4.5_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 4.2_
## 6. CI batch and randomized seed sweep
- [x] 6.1 RED+GREEN: randomized M-seed sweep with zero violations
- Test: run `raft_sim_run` for a fixed `max_steps` across M seeds (M ≥ configured minimum) plus all targeted scenarios; fail if any seed/scenario violates any invariant
- Implement the sweep harness in `test_raft.c`; bound `max_steps`, M, and array sizes so the target fits the library test-suite budget
- Observable completion: the full batch runs clean and within budget
- _Requirements: 6.1, 6.2, 6.4_
- _Boundary: test_raft.c_
- _Depends: 5.1, 5.2, 5.3, 5.4, 5.5_
- [x] 6.2 Verify the conformance batch is ASAN/UBSAN-clean
- Run the conformance batch under the library test sanitizer build; fix any leak/overflow/UB in the harness (fixed-size arrays, no allocation)
- Observable completion: `/xce-build lib` sanitizer run reports no memory errors, leaks, or UB for the conformance target
- _Requirements: 6.3_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 6.1_
## 7. Deliberately-broken-build negative test (the oracle's teeth)
- [x] 7.1 RED+GREEN: compile-time fault switch emulates the bug-851 defects in the adapter
- Implement `RAFT_SIM_BREAK_TERM_STAMP` (adapter `on_persist_entry` writes term 0) and `RAFT_SIM_BREAK_TRUNCATE` (adapter `on_truncate_from` no-op), confined to the test/harness build configuration; default off
- Observable completion: with a macro off, scenarios are clean; the macro is not set in the default CI run and does not touch `raft.c`
- _Requirements: 7.1, 7.3_
- _Boundary: raft_sim.c_
- _Depends: 5.4, 5.5_
- [x] 7.2 RED+GREEN: negative test asserts both directions
- Test: under the broken configuration the Figure-8 / divergent-suffix scenario triggers a Log Matching / Leader Completeness / State Machine Safety violation (with trace dump); under the correct configuration the same scenario passes — both asserted in one test
- Observable completion: the broken build is caught and the correct build passes, proving checker sensitivity
- _Requirements: 7.1, 7.2, 7.4_
- _Boundary: test_raft.c_
- _Depends: 7.1_
## 8. Optional TLC-consumable trace export
- [x]* 8.1 TLC-consumable trace export (deferred; off by default)
- Implement `raft_sim_export_trace` gated by `RAFT_SIM_EXPORT`: per step, emit the action + tracked `raft.tla` variables (`messages` summary, per-node `currentTerm`/`state`/`log`/`commitIndex`) named per `raft/raft.tla`, in a stable line-oriented format; off by default
- Observable completion: with `RAFT_SIM_EXPORT` set, a run emits a stable, spec-named record stream; default runs emit nothing extra
- _Requirements: 8.1, 8.2, 8.3_
- _Boundary: raft_sim.c, test_raft.c_
- _Depends: 4.2_
```
## Implementation Notes
- **Task 4.2 — UPSTREAM raft.c BUG SURFACED (bug-1095/1096, bug-851 family).** While building
the scheduler, the conformance oracle detected a real split-brain defect in `raft.c`
`raft_on_vote_reply`: (1) vote grants are tallied with a plain counter that ignores
`from_idx`, so a legitimate `DuplicateMessage` of a vote-response double-counts toward
quorum; (2) there is no stale-term (`mterm < currentTerm`) reply drop. In `raft.tla`
`votesGranted` is a SET (idempotent under the legal Duplicate action) and
`DropStaleResponse` discards stale replies — so a correct implementation must tolerate
both. Result: two leaders at the same term (Election Safety violation) on some seeds.
This is OUT OF THIS SPEC'S BOUNDARY (raft.c protocol logic, owned by raft-core /
ha-term-safety). The harness must NOT mask it (R2.6). **Consequence for downstream
tasks:** any task asserting "zero invariant violations on the correct build" (5.x targeted
scenarios, 6.1 M-seed sweep, 7.2 correct-direction) may surface this defect. Implementers
of those tasks MUST NOT weaken the oracle or hand-pick seeds to hide the bug; if a
legitimate scenario surfaces it, report BLOCKED with the upstream cause rather than
working around it. Seed 3 is clean over 600 steps and is used only to prove the 4.2
scheduler mechanism reaches all action kinds.
- **UPDATE (post-4.2): bug-1095/1096 FIXED in raft.c** (commit `fix(raft): vote-reply
per-peer dedup + stale-term drop`). `raft_on_vote_reply` now tracks grants in a per-peer
`vote_replies[]` set (idempotent under DuplicateMessage) and drops stale-term replies
(DropStaleResponse), mirroring the existing pre-vote idiom; 47 raft / 264 lib tests pass.
The library is now correct, so downstream tasks (5.x scenarios, 6.1 M-seed sweep, 7.2
correct-direction) CAN and MUST assert zero invariant violations on the correct build.
The user authorized this cross-boundary fix. The deliberately-broken-build fault switch
(task 7) remains the way to deliberately re-introduce a divergence for the negative test.
- **Task 5.1 — scenario commit-propagation pattern (applies to 5.2–5.5).** A follower's
`commit_index`/`applied[]` only advance when it RECEIVES an AppendEntries carrying a
`leader_commit` greater than its own (the library applies it). A scenario that replicates
entries and calls `raft_advance_commit` on the LEADER, but never sends a follow-up
AE/heartbeat round afterward, leaves followers at `commit_index=0` — making any per-follower
commit-prefix / State-Machine-Safety / Leader-Completeness assertion VACUOUS on the follower
side (5.1 has this weakness; Log Matching still validates log convergence there). For
commit-critical scenarios — ESPECIALLY 5.4 (Figure-8) where follower commit advancement is
the whole point — after `raft_advance_commit` on the leader, drive ANOTHER AppendEntries
round so followers observe the new `leader_commit` and advance, then assert follower
`commit_index`/`applied[]` actually moved. Do NOT assert convergence via a loop bounded by
`min(commit_index)` when followers are at 0 (it silently passes). 5.1 was accepted as-is
(meets R4.1 via throughout-checking + log matching); 5.2–5.5 should propagate commit so
their commit assertions are non-vacuous.
- **CORRECTION (task 5.2, confirmed): this library uses a LEADER-ONLY commit model.**
`raft_on_append_entries_request` does `(void)leader_commit;` (raft.c:646-655) — a FOLLOWER
never advances its `commit_index`/`applied[]` from AppendEntries; `on_commit_advanced`
fires ONLY on the leader via `raft_advance_commit` (raft.c:816). This is intended XCE
design (follower commit visibility is a controller/heartbeat concern — see
ha-heartbeat-committed-propagation — not the raft library). CONSEQUENCE for scenarios: a
follow-up AE round does NOT advance follower commit; do NOT assert follower
`commit_index == leader commit_index`. Assert re-convergence over the LOG instead
(every node's `log_count` reaches the leader's tip + per-entry term match / Log Matching),
and assert leader-side commit via `leader.commit_index`. For 5.4 (Figure-8), the property
under test is LEADER-side (the leader must not mark a previous-term entry committed until a
current-term entry commits) — driven by `raft_advance_commit` on the leader, which works.
applied[]/SM-Safety is exercised across leader CHANGES (each node accumulates applied[]
while it is leader). This SUPERSEDES the follower-commit-propagation guidance above.
- **PROCESS FIX (task 5.3 commit e445404 was BROKEN).** That commit captured the
drop_dup_reorder decl (raft.h) + test (test_raft.c) but NOT its implementation in
raft_sim.c — a stray `git checkout` (a reviewer cleaning up the `src/build` symlink)
reverted the uncommitted impl between review and commit, so the lib build was broken
(undefined reference) from e445404 onward. Detected during task 5.4. The exact
implementation was recovered (byte-identical) and re-inserted into raft_sim.c; it is
committed together with task 5.4. LESSON: after every task commit, verify `git show
--stat` includes every expected source file (raft_sim.c was silently absent from
e445404's stat). Reviewers must use targeted `git checkout <path>` (never `git checkout .`
/ `git stash`) so uncommitted impl is never reverted.