Skip to content

Remove the duplicated logic that let the code, the models and the tests drift - #24

Merged
jaredLunde merged 1 commit into
mainfrom
jared/no-drift
Oct 2, 2026
Merged

jaredLunde merged 1 commit into
mainfrom
jared/no-drift

Conversation

@jaredLunde

@jaredLunde jaredLunde commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Removes the places where the same logic lived twice and could drift: in the models and the code, in two cursor_ranks, in three NATS resume paths, in three test harnesses. Also splits the 645-line watch_applied body. Behavior is unchanged; the API changes are below.

Models run production logic

Every decision the repair rests on is now a kernel in protocol.rs, called by both the production code and the Stateright models:

  • listing_is_truth
  • plan_repair (RepairMode → RepairPlan)
  • cursorless_start_needs_repair
  • restore_key (KeyState → KeyRestore)
  • restore_ahead

tests/model.rs, model_repair.rs, model_fleet.rs and model_live_watch.rs drop their hand copies (listing_is_truth, restores, restore_op).

A model now expresses a mutation by forging a kernel's input, never by re-implementing the decision:

  • RelistOnEvicting: the listing reads as the truth.
  • UnanchoredRelist: the fold reads as empty.
  • RestoreIgnoresVersions: both sides read as revisionless.
  • RestoreIgnoresListing: the key reads as not listed.

All mutations are still caught. model_repair now also checks Auto on a bucket that keeps current values.

watch_applied split

  • Fold replaces the flush!/ingest!/repair_fatal! macros. Fold holds the batch, the applied cursor and the store, and every change goes through it: ingest, correct, advance_to, flush, settle, export. The cursor-after-apply invariant is enforced in one place. The select loop is about 100 lines.
  • Both halves of the expiry repair live in repair.rs. The watch task side is run_watch, plan, the listing and the artifact checks. The main loop side is fold_in, relist and restore, which act on the Fold.
  • #[allow(unused_assignments)] is gone. It only existed for the macros' dead stores.

Other duplication

  • WatchCursor::rank() replaces the two cursor_rank copies (repair, transport).
  • SnapshotStore::has_entries() replaces fold_has_entries, which used an Err to stop a scan early. fjall and RocksDB read one entry. New conformance check on all three backends.
  • NatsKvWatcher::create_resume is the one place the three *_from paths check the resume window. It runs the pre-check, the creation with its expired-cursor error classification, and the post-check.
  • tests/common is split into nats.rs, minio.rs and crash.rs. Only crash.rs needs transport.
    • tests/integration.rs and src/nats.rs's floor-guard tests use the shared server: the unit tests include nats.rs via #[path].
    • The two private copies of the spawner are gone.

API changes (land in 0.8.0)

  • Snapshot::stale_keys(current_keys, retention) now returns Option<Vec<&str>>. It returns None when retention says the listing isn't the truth, which is the same planner and rule as ExpiryRepair::Relist. It was a raw-API path to the bug Repair expired cursors from artifacts on buckets that evict current values #21 fixed.
  • SnapshotStore::has_entries is a new provided method.
  • protocol gains the kernels listed above.

CI guards against drift

  • mise run format:check (dprint: rustfmt + yamlfmt). Unformatted code no longer waits for the next cargo fmt to reformat someone else's diff. That was the tests/dst_invariants.rs drift, now formatted.
  • cargo doc with -D warnings. Three pre-existing broken intra-doc links are fixed.
  • cargo check --tests per optional feature, so shared test infrastructure can't quietly depend on another feature.

Verification

  • cargo clippy --all-targets --all-features -D warnings and cargo doc -D warnings: clean.

  • Every feature combination compiles with its tests: none, fjall, rocksdb, transport, fjall+transport.

  • Full suite, both CI configurations: green locally. All features: 17 binaries (lib 124, integration 46, snapshot_store 73 with the new has_entries check on all three backends, repair_dst on all three backends, live MinIO). Default features: green.

  • Models (release): the state spaces are unchanged. The default tiers ran on main and on this branch:

    • All 39 configurations on main, mutation runs included, have identical state and unique-state counts here. The only extra configuration is the one this PR adds (keep-current, Auto: 54,247 states).
    • Every mutation is caught by the same properties as on main.
    • The deep tiers therefore can't differ either. deep_evicting_more_revisions passes.
    • deep_more_revisions and deep_three_exporters exceed this 27 GB machine's memory; the runs were killed.
  • tests/repair_dst.rs catches all nine code mutations of the restructured repair, each with a counterexample:

    # Mutation Caught by
    M1 no pre-repair settle retry fold ≠ every write minus every real delete
    M2 no backlog drain on_applied went backward
    M3 cursor committed before the restore diff fold ≠ every write minus every real delete
    M4 Auto trusts the listing a repair deleted live keys
    M5 restore never deletes fold ≠ every write minus every real delete
    M6 no ack barrier fold ≠ every write minus every real delete
    M7 unanchored fold looks empty fold ≠ every write minus every real delete
    M8 restore ignores the live listing a repair deleted live keys
    M9 restore ignores versions apply saw a revision regress

🤖 Generated with Claude Code

https://claude.ai/code/session_0152kQKDdP8XRhoeqisJYpWr

…ts drift

Every decision the expiry repair rests on is now a kernel in protocol.rs
that production and the Stateright models both call: listing_is_truth,
plan_repair, cursorless_start_needs_repair, restore_key, restore_ahead.
The models' hand copies are gone; their mutations forge kernel inputs
instead of re-implementing decisions, and every one is still caught.

watch_applied's body is split: a Fold struct (batch, applied cursor,
store; ingest/correct/advance_to/flush/settle/export) replaces the
flush!/ingest!/repair_fatal! macros, and both halves of the repair (the
watch task's and the main loop's) live in repair.rs.

Also: WatchCursor::rank replaces two cursor_rank copies;
SnapshotStore::has_entries replaces the error-as-early-stop scan (one
entry read on fjall/RocksDB; conformance-checked on every backend);
NatsKvWatcher::create_resume is the one resume-window check for the three
*_from paths; Snapshot::stale_keys takes the bucket's retention and
returns None where the listing isn't the truth; tests/common splits so
only crash injection needs `transport`, and integration.rs and the
floor-guard unit tests share its nats-server instead of copying it.

CI now runs dprint check, rustdoc with -D warnings (three broken links
fixed) and a per-feature `cargo check --tests`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0152kQKDdP8XRhoeqisJYpWr
@jaredLunde
jaredLunde merged commit fcb4c73 into main Oct 2, 2026
1 check passed
@jaredLunde
jaredLunde deleted the jared/no-drift branch October 2, 2026 02:18
@jaredLunde jaredLunde mentioned this pull request Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant