core(mailbox): rebuild from kameo reference — zero-box Signal queue + backpressure (#112)#132
Closed
joeldsouzax wants to merge 6 commits into
Closed
core(mailbox): rebuild from kameo reference — zero-box Signal queue + backpressure (#112)#132joeldsouzax wants to merge 6 commits into
joeldsouzax wants to merge 6 commits into
Conversation
Start the part-by-part core rebuild (epic #122) in a fresh, god-level-bar crate rather than mutating the vendored kameo modules. The mailbox is the first spine part (#112), built standalone so it can be hard-tested before the run-loop is grafted onto it (#114/#116/#117). - `Mailboxed { type Msg }`: the scaffold seam the rebuilt `Actor` trait will later subsume. - `Signal<A>` = `Message(A::Msg) | Stop`: a concrete, closed envelope with no `Box<dyn>` — a `tell` moves the message into the queue slot (zero per-message alloc). - `Capacity(NonZeroUsize)`: makes both illegal capacities unrepresentable (0, and > tokio's MAX_PERMITS which panics), so `bounded` is infallible — validated at our own boundary rather than relying on tokio's panic. - bounded `send`/`try_send`/`recv`; `SendError`/`TrySendError::{Full,Closed}` carry the undelivered signal back to the caller. TDD: 5 tests (round-trip, backpressure hand-back, capacity boundaries, Stop-after-message FIFO). Clippy `--all-features` clean.
Continue the standalone mailbox (#112) through the behaviour-test tier. Production additions: - `close()`: graceful stop — reject new sends, still drain what's queued. - `Signal::LinkDied(Box<LinkDied>)`: the first control arm, deliberately boxed. A `size_of` test pins `Signal<Probe>` at 16 bytes, proving the cold fat variant does not inflate the hot message slot (large-variant discipline). - `WeakMailboxSender` + `downgrade`/`upgrade` and `Clone` on the sender: the non-pinning liveness handle death-watch is built on — `upgrade()` is `None` once the last strong sender drops. (This keeps kameo's WeakSender approach, the reason the generational-arena was dropped in #122.) - scaffold `ActorId`/`StopReason`/`LinkDied` placeholders (owned by #121/#113/ #120), present only to give the LinkDied arm a concrete shape. Tests (TDD): +10 → 15 total. Lifecycle (close-drains, send-after-drop, recv-none-after-drain), LinkDied round-trip + slot-size guard, weak-sender tracks-last-strong + upgraded-can-send, a real multi-thread linearizability test (8 senders × 64 msgs, Barrier-synced, FIFO-per-sender + no loss/dup), and two proptests (capacity boundaries incl. MAX±1/usize::MAX; single-sender FIFO round-trip over arbitrary sequences × capacities). Clippy --all-features clean.
Measure, don't assume, and wire the tooling into the pinned flake so every check is reproducible (no floating `nix run nixpkgs#…`). - criterion bench (`benches/mailbox.rs`): zero-box `tell` enqueue ≈ 5.6 ns; send+recv round-trip ≈ 21.4 ns. Baseline for the #114/#118 two-tier wiring. - cargo-mutants: added to the devShell (pinned) and exposed as an on-demand `packages.mutants` (`nix build .#mutants`, mirrors the coverage package) that fails if any mutant survives. Result on mailbox.rs: 0 missed — 6 caught, 30 unviable, 4 detected-via-timeout. - Killed the 2 initial survivors (the hand-written SendError/TrySendError Debug impls) with an explicit Debug-format assertion. Tests: +1 → 16. Clippy --all-features clean; flake nixfmt-clean.
…erral (#112) Add a bombay-core section to the coverage baseline: the `nix build .#mutants` reproducible mutation gate, the mailbox test/bench summary, and the decision to defer loom/shuttle DST to #116/#120 — the real tokio::sync::mpsc is opaque to both model checkers, so they land with the first bombay-owned concurrency (run-loop select / death-watch push), not on the tokio wrapper.
Measured guard for the zero-box decision's worst case: size_of::<Signal<A>>() = the largest Msg variant, so one fat inline command variant taxes every queue slot. Measured: small=16B, fat inline([u8;4096])=4104B, boxed=16B -> 4.10 MB vs 16 KB for 1_000 queued messages (256x). Boxing the fat variant (as Signal boxes LinkDied) restores the small slot. Documents + guards the tradeoff for #122.
Contributor
Author
|
Superseded by the principal-grade redesign in #133. The v1 mailbox here shipped without the channel-primitive survey #112's contract required and with a non-composing API; #133 does the evaluation (ADR-0001 → flume, measured ~2× faster than the tokio default this PR used), adds the |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #112. First part of the M1 core-rebuild epic (#122): the mailbox, built standalone in a fresh
bombay-corecrate (born under the god-level bar — no #61 quarantine) so it can be hard-tested before the run-loop is grafted on (#114/#116/#117).What
Signal<A>=Message(A::Msg) | Stop | LinkDied(Box<LinkDied>)— a concrete, closed envelope, noBox<dyn>.tellmoves the message into the queue slot (zero-alloc), vs kameo'sBox<dyn DynMessage>heap-box per message.Capacity(NonZeroUsize)validated1..=MAX_PERMITS→boundedis infallible. Both illegal capacities are unrepresentable: tokio panics on0and on> usize::MAX>>3(verified empirically), so we validate at our own boundary instead of relying on tokio's panic (rule 4).send/try_send/recv/close;SendError/TrySendError::{Full,Closed}hand the undelivered signal back (rule 3).WeakMailboxSender— non-pinning liveness (upgrade()isNoneonce the last strong sender drops). Keeps kameo'sWeakSender, the reason the generational-arena was dropped in epic: rebuild the runtime core part-by-part (kameo = reference oracle) #122.LinkDiedboxed; asize_ofguard pinsSignal<Probe>at 16 bytes.Rigor (17 tests, TDD)
Sequence/FIFO · lifecycle (close-drains / send-after-drop / recv-none) · defensive boundary (proptest
0,1,MAX-1,MAX,usize::MAX) · 8-threadBarrierlinearizability (FIFO-per-sender + no loss/dup) · single-sender FIFO proptest · a measured worst-case demonstration of the monomorphic slot-size tax.benches/mailbox.rs):tellenqueue ≈ 5.6 ns, send+recv ≈ 21.4 ns.nix build .#mutants(cargo-mutants pinned via the flake). 0 missed onmailbox.rs.tokio::sync::mpscis opaque to both model checkers (they only instrument their own primitives), so they land with the first bombay-owned concurrency (run-loopselect/ death-watch push), not on the tokio wrapper. Recorded on core(mailbox): rebuild from kameo reference — queue + Signal + backpressure #112 +docs/testing/coverage-baseline.md.Reproducibility
Every check runs through the pinned flake — cargo-mutants added to the devShell +
packages.mutants; no floatingnix run nixpkgs#….Design risk filed
The monomorphic zero-box decision's worst case (silent fat-variant memory/copy blowup — measured 4.10 MB vs 16 KB for 1k queued msgs; closed-enum expression problem for heterogeneous/M3 group addressing) is documented on #122 with carry-over actions on #114.
nix flake checkgreen locally (exit 0, full matrix).