Skip to content

feat(publish): prototype generation-directory publish (not wired), with experiments and a model check - #275

Merged
hyperpolymath merged 4 commits into
mainfrom
claude/gallant-bell-4bo4oy-l-gen-dir
Nov 9, 2026
Merged

hyperpolymath merged 4 commits into
mainfrom
claude/gallant-bell-4bo4oy-l-gen-dir

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Prototype, kept as a draft until the maintainer decides the open design questions. Nothing calls it yet.

Why

On #264 the maintainer chose to replace per-file output writes with a generation-directory publish. This prototype tests that design on evidence first.

The publish is meant to close three gaps that exist in every output mode today:

  • Check-then-write race: a symbolic link swapped in between the preflight check and the write.
  • Partial files after a crash.
  • Interleaving between concurrent runs.

What is here

src/publish.rs

For a report D/S.json, the layout is:

  • D/S.gen/<id>/: one complete generation per directory.
  • D/S.gen/CURRENT: a JCS manifest naming the current generation and the SHA-256 of each of its files.
  • Mirrors at D/S.json and the other output names: hard links into the current generation, with copies where linking fails.

A publish runs in this order:

  1. Write the generation into <id>.partial/, syncing each file.
  2. Rename it to <id>.
  3. Under a File::lock:
    • refuse if CURRENT names a newer generation (ids sort by run start);
    • otherwise replace CURRENT by temp file and rename;
    • refresh the mirrors;
    • collect old generations (default: keep 1 older).

Readers take no lock. When the generation CURRENT named has been collected, they retry from CURRENT.

examples/publish_stress.rs

Compares three strategies: inplace (fs::write, as on main), perfile (rename each file) and genset (this prototype). It runs a multi-process race, kill -9 crash trials and a latency bench.

tests/publish_model.rs

An exhaustive explicit-state check of the protocol, reported as TESTED with these bounds:

  • up to 3 writers and 1 reader, 2 files;
  • a crash at every writer step;
  • garbage collection of crashed and live writers' partial generations;
  • 13,427 states at most.

Results:

  • P1–P3 hold: CURRENT names only complete generations, it never moves backwards, and manifest readers never fail or return a partial set.
  • P4 fails, as expected: after a crash the mirrors can be torn.
  • Three deliberate mutants are each caught, showing that the invariants are not trivially true.

Measured on Linux 6.18, ext4, 4 cores

inplace perfile genset (mirrors) genset (manifest)
race, 3 writers × 150 + readers: partial reads 7931 / 8424 0 / 3209 0 / 6058 0 / 2500
race: torn sets 182 3008 67 0
kill -9, 300 trials: partial / torn left behind 176 / 13 0 / 155 0 / 3 0 / 0
100 races: torn final state 0 7 0 0
p50 latency, 1 KiB / 256 KiB / 4 MiB 0.27 / 1.0 / 12.5 ms 1.2 / 3.2 / 23.8 ms 2.7 / 4.6 / 33.7 ms

Defects the experiments found, each fixed here with a test that fails without the fix:

  • GC raced a writer renaming its own .partial directory, so a writer crashed with NotFound in 1 of 40 races.
  • A reader could lose a generation to GC: 1 in 2,500 reads. It now retries, and was 0 in about 11,200 reads afterwards.

Open before this can be wired

  • Mirrors torn after a crash (P4). The options are to document it, to use one current symlink pointer (crash-consistent on Unix), or to drop the mirrors.
  • Proof legs under PROOF-POLICY §1.2 and §4:
    • a TLA+ spec checked by TLC (the policy's worked exception), which was not runnable in this session;
    • Creusot/Pearlite and Verus for the pure selection logic, still OPEN.
  • Other questions:
    • the generation id format (UUIDv7 or v8 per the estate ID policy);
    • Windows behaviour (no Windows run yet);
    • power-loss durability (fsync ordering is not tested; kill -9 tests process crashes only);
    • what a replacement keeps: ACLs, xattrs, labels and ownership.

Generated by Claude Code

…ts and a model check

PROTOTYPE, not wired into any subcommand. It follows the maintainer's
decision on #264 to replace per-file writes with a generation-directory
publish. It is here so the design can be judged on evidence before any
output path uses it.

- src/publish.rs: write a generation into <stem>.gen/<id>.partial/,
  rename it complete, then under an advisory lock move the CURRENT
  manifest (JCS, with SHA-256 per file) unless a newer generation is
  already current. Mirrors at the old paths are refreshed by hard link
  and rename; old generations are collected. Lock-free readers retry
  when a generation is collected under them.
- examples/publish_stress.rs: multi-process race, kill -9 crash and
  latency experiments comparing in-place fs::write, per-file rename and
  generation publish.
- tests/publish_model.rs: an exhaustive explicit-state check of the
  protocol (up to 3 writers, 1 reader, a crash at every step, garbage
  collection of live and crashed writers' partial generations). P1–P3
  hold. P4 (mirrors consistent after a crash) fails as expected, matching
  the crash experiment. Three mutants show the checker detects broken
  protocols.

Two defects the experiments found are fixed here, each with a test that
fails without the fix: GC raced a writer's rename of its own partial
directory, and a reader could lose a generation to GC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PFaqv6iBXrt3W5Akd1Fq5k
@coderabbitai

coderabbitai Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 57 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 829fdb42-9d5e-4c28-9c7c-453fd72afcd7
Commits

Reviewing files that changed from the base of the PR and between e12b823 and 83ce389.

Files selected for processing: 17

  • .github/workflows/verify.yml
  • PROOF-NEEDS.adoc
  • docs/design/GENERATION-PUBLISH.adoc
  • examples/publish_stress.rs
  • src/lib.rs
  • src/publish.rs
  • tests/publish_model.rs
  • verification/proofs/tlaplus/GenerationPublish.tla
  • verification/proofs/tlaplus/MirrorsCrash.cfg
  • verification/proofs/tlaplus/MirrorsNoCrash.cfg
  • verification/proofs/tlaplus/MutantCollectCurrent.cfg
  • verification/proofs/tlaplus/MutantNoSupersede.cfg
  • verification/proofs/tlaplus/MutantSwapBeforeComplete.cfg
  • verification/proofs/tlaplus/NoRetry.cfg
  • verification/proofs/tlaplus/Safety3Keep0.cfg
  • verification/proofs/tlaplus/Safety3Keep1.cfg
  • verification/proofs/tlaplus/check.sh
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Comment thread examples/publish_stress.rs Fixed
Comment thread examples/publish_stress.rs Fixed
Comment thread examples/publish_stress.rs Fixed
Hypatia flagged 25 unwrap() calls, one expect() and one panic! in
examples/publish_stress.rs (CWE-754). The example now returns
anyhow::Result throughout: bad arguments and I/O failures exit with a
message and status 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PFaqv6iBXrt3W5Akd1Fq5k
…status rows

- verification/proofs/tlaplus/GenerationPublish.tla: the protocol for TLC,
  step for step like tests/publish_model.rs. Eight configs, each stating
  its expected outcome. P1–P3 pass for 3 writers with keep 0 and 1 (up to
  17,744 distinct states). P4 passes without crashes and fails after one,
  the documented mirror limit. The reader without a retry fails P3. Three
  mutants each fail P1 or P2.
- check.sh checks the jar's SHA-256 (tla2tools v1.7.4, TLC 2.19) and fails
  on any unexpected outcome. It fails both ways: on a wrong jar, and on a
  planted wrong expectation.
- verify.yml gains a `tlaplus` job that runs it, recording a PROOF-POLICY
  §6 deviation (TLC is not in Guix or the container yet). actionlint
  1.7.7 is clean, and the job uses only the already-locked checkout pin.
- The "swap before complete" mutant now moves the pointer when the partial
  generation is created, in both models, matching its name.
- docs/design/GENERATION-PUBLISH.adoc: layout, protocol, guarantees,
  measurements and open questions. It records the owner's decision to keep
  hard-link mirrors and document the crash limit.
- PROOF-NEEDS.adoc: a TESTED row for the protocol, with bounds and the §4
  exception, and an OPEN Creusot/Pearlite + Verus row for the pure
  decisions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PFaqv6iBXrt3W5Akd1Fq5k
@hyperpolymath
hyperpolymath marked this pull request as ready for review November 9, 2026 15:46
@chatgpt-codex-connector

Copy link
Copy Markdown
Codex Review Summary: ❌ Failed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ❌ Failed 2026-11-09T15:46:33.073247Z
We couldn’t complete this review. Comment @codex review to try again.
48296bb Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@hyperpolymath
hyperpolymath enabled auto-merge (squash) November 9, 2026 22:51
@sonarqubecloud

sonarqubecloud Bot commented Nov 9, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@hyperpolymath
hyperpolymath merged commit d4a68ed into main Nov 9, 2026
66 of 68 checks passed
@hyperpolymath
hyperpolymath deleted the claude/gallant-bell-4bo4oy-l-gen-dir branch November 9, 2026 22:52
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.

4 participants