Skip to content

test(core): split the Era handoff Loom model into offer / accept / cancel - #255

Merged
dshbox-dev merged 2 commits into
mainfrom
test/era-offer-accept-model-20261005
Oct 5, 2026
Merged

dshbox-dev merged 2 commits into
mainfrom
test/era-offer-accept-model-20261005

Conversation

@dshbox-dev

@dshbox-dev dshbox-dev commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Closes #247

Summary

The ER-07 handoff model in crates/cordis-core/tests/loom_era.rs used to CAS WAITING -> DELIVERED in one step. Production (FiberHandle::era_swap) sends an EraHandoff whose EraHandoffGuard stays armed inside the offered message until EraHandoff::accept disarms it. That gives production an offered-but-not-accepted state, which the old model did not have.

  • Model: a HandoffGuard value with a Drop (EraHandoffGuard, Option::take single-use) moves inside an EraOffer (EraHandoff) through a reduced oneshot slot, a loom Mutex<OfferSlot> that holds the stored offer and a receiver-dropped flag. The mutex stands in for the oneshot's send-to-receive synchronization. Tokio's oneshot internals are trusted, not modeled, and the model contains no unsafe. The oracle facts use Relaxed and are read only after the joins. (Changed in e1eb528 after maintainer review; the first revision copied Tokio's state word into an UnsafeCell.)
  • Actions:
    • offer: let _ = send.send(Ok(offer)). A refused offer is dropped while still armed.
    • accept: one poll of receive.await followed by handoff.accept() in the same poll.
    • cancel: the receiver is dropped. This can land before the offer (the sender gets the offer back) or after it (the receiver releases the unaccepted offer).
  • Assertions: every terminal state has exactly one owner.
    • Accepted: 0 framework cleanups.
    • Cancelled before the offer: exactly 1 guard cleanup.
    • Cancelled after the offer but before accept: exactly 1 guard cleanup.
    • The outcome must agree with the send result.
    • The positive test also asserts that all three outcomes are reached across the explored schedules.
  • Negative controls:
    • offered_but_unaccepted_handoff_without_guard_cleanup_is_detected (new) treats a successful send as delivery. It fails with offered but unaccepted successor must be cleaned exactly once by its armed guard.
    • cancelled_handoff_without_framework_cleanup_is_detected is unchanged in name and message. It now disarms a refused offer.
    • cancellation_reclaiming_an_already_handed_off_successor_is_detected is re-targeted to after accept. Its message is now accepted successor cannot retain framework cleanup ownership.
  • Bounds: era_model is unchanged (max_threads = 3, max_branches = 64). No test was renamed, and CI does not select any loom_era test by name.
  • Docs:
    • docs/lifecycle-concurrency-modeling.md: removed the "Era offer/accept window" entry from the not-represented list, reworded the Phase 3 ER-07 handoff note and the controls bullet that follows it, and changed "twelve" to "thirteen" Era tests. The ready() bullets and LC-06 text, which Loom: model ready() publication sequence in ReadyHistoryModel (LC-06) #248 is editing, are untouched.
    • docs/lifecycle-concurrency-review-guide.md: added the new control to the ER-07 row and updated the Era test count.
  • No production code or public API change.

Gates (ci/gates.sh t247)

PASS: toolchain, fmt, clippy, vocab, doc. The test, examples and latest gates were not run locally for this test/doc-only change. cargo clippy --release -p cordis-core --features loom-tests --test loom_era reports nothing for the test file.

Loom timings (local, CI commands)

  • cargo test --locked --release -p cordis-core --features loom-tests --test loom_era: 13 passed. The tests take 0.20 s, or about 0.4 s wall with the build already done (re-measured at e1eb528).
  • ... --test loom_inertia: 16 passed. The tests take 11.2 s, or about 14.2 s wall.

Both are well inside the 15-minute Loom job timeout.

🤖 Generated with Claude Code

dshbox-dev added a commit that referenced this pull request Oct 5, 2026
Maintainer review of #255: the UnsafeCell-backed copy of Tokio's oneshot
state word would have been the repo's first `unsafe`, and it created a
correspondence target that drifts with Tokio upgrades. The ticket's
subject is guard ownership across offer/accept, not oneshot memory
ordering, which is trusted to Tokio.

The reduced oneshot is now a loom `Mutex<OfferSlot>` holding the stored
offer and a receiver-dropped flag. Send refuses and returns the still-armed
offer when the receiver was dropped; receiver drop marks the slot closed
and drops any stored offer; one poll takes the offer and accepts it. The
mutex stands in for the oneshot's send-to-receive synchronization. The
guard remains a real value with Drop/Option::take. Assertions, the
all-outcomes-reached check, the three negative controls and their
messages, test names and era_model bounds are unchanged.

Refs #247

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dshbox-dev and others added 2 commits October 5, 2026 02:54
…ncel

The ER-07 handoff model CASed WAITING -> DELIVERED in one step, so its
"delivered" stood for production's accept and the offered-but-not-accepted
state was absent (#247). Production sends an EraHandoff whose
EraHandoffGuard stays armed inside the offered message until
EraHandoff::accept disarms it; dropping the message unaccepted runs the
guard's Drop, which detaches cleanup_undelivered_successor.

The model now carries a guard value with a Drop through a reduced oneshot
that keeps Tokio 1.53's VALUE_SENT/CLOSED state-word protocol and
orderings (AcqRel/Acquire set_complete CAS, Acquire poll, Acquire
fetch_or close), with the message in a loom UnsafeCell so Loom checks the
send/receive edge for causality. Oracle facts are Relaxed and read only
after the joins, so no happens-before edge is added. The framework offers;
the caller polls once and accepts in that poll, or drops the receiver,
which lands before or after the offer. Every terminal state has exactly
one owner: accepted (no framework cleanup) or cancelled before/after the
offer (exactly one guard cleanup). The positive test also checks that all
three outcomes are reached across the explored schedules.

offered_but_unaccepted_handoff_without_guard_cleanup_is_detected treats a
successful send as delivery and must fail with the cancel-after-offer
assertion. The two existing ER-07 controls keep their names; the reclaim
control is re-targeted to after accept. era_model bounds are unchanged.

Docs: the "Era offer/accept window" entry leaves the not-represented
list, the Phase 3 ownership note describes the split model, and the Era
test count becomes thirteen (also in the review guide's ER table).

Closes #247

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Maintainer review of #255: the UnsafeCell-backed copy of Tokio's oneshot
state word would have been the repo's first `unsafe`, and it created a
correspondence target that drifts with Tokio upgrades. The ticket's
subject is guard ownership across offer/accept, not oneshot memory
ordering, which is trusted to Tokio.

The reduced oneshot is now a loom `Mutex<OfferSlot>` holding the stored
offer and a receiver-dropped flag. Send refuses and returns the still-armed
offer when the receiver was dropped; receiver drop marks the slot closed
and drops any stored offer; one poll takes the offer and accepts it. The
mutex stands in for the oneshot's send-to-receive synchronization. The
guard remains a real value with Drop/Option::take. Assertions, the
all-outcomes-reached check, the three negative controls and their
messages, test names and era_model bounds are unchanged.

Refs #247

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dshbox-dev
dshbox-dev force-pushed the test/era-offer-accept-model-20261005 branch from e1eb528 to d64e3e7 Compare October 5, 2026 02:54
@dshbox-dev
dshbox-dev merged commit d77cf91 into main Oct 5, 2026
10 checks passed
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.

Loom: split Era handoff into offer / accept / cancel-after-offer

1 participant