diff --git a/ROADMAP.md b/ROADMAP.md index 152b925..ce05af2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -152,6 +152,79 @@ Landed as two primitives, *without* the sketched monitor machinery: --- +## v0.7 — select, on epoch-stamped consuming wakes + +Goal: cross-channel wait (`select`) without surrendering the precise-wake +invariant. Decision record: the alternative ("wakes are hints", loop-and- +recheck at every park site) was evaluated and rejected — it distributes the +soundness obligation across every present and future park site and turns +every stale wake into a wasted reschedule. Instead the slot protocol itself +gains *wait identity*: a park-epoch packed into the spare bits of the slot +word, so that a stale unpark dies at one failed CAS in the waker. + +Protocol (the consuming-wake rules): +- Word becomes `(gen << 32) | (epoch << 8) | state` — 24-bit epoch in the + previously unused bits; every transition still one CAS, gen check still + atomic with it. +- `begin_wait` (actor-side, before registering with any waker) bumps the + epoch and returns it; registrations carry `(pid, epoch)`. +- Every successful wake transition **consumes** the epoch (bumps it): + `Parked(e) → Queued(e+1)`, `Running(e) → RunningNotified(e+1)`. A second + unpark stamped with the same epoch can never land — at most one wake per + wait, by construction. This is what makes select's *loser* arms harmless + without a cancellation pass: their stale registrations fire one futile CAS + on next send and self-clean. +- `unpark_at(pid, epoch)` = epoch-matched wake. Bare `unpark(pid)` remains as + the wildcard and is reserved for **terminal** wakes (`request_stop`), which + never return control to the code that parked. +- One-shot park sites (`Mutex::lock_timeout`, `sleep`, `block_on_io`, + `wait_fd`) stay exactly as written: precise wakes are preserved, so a wake + still *means* something. + +### 1. Slot-word epoch + consuming unparks +`slot_state.rs`: repack, `begin_wait`, epoch-matched/consuming `unpark`, +epoch-preserving claim/yield/park transitions. Loom: existing theorems +re-proved on the new word + a new stale-epoch theorem (an unpark stamped with +a consumed epoch can never enqueue *or* notify). Plumbing only — no caller +stamps yet; behaviour identical. + +### 2. Stamp every registration-based wake; retire per-primitive wait seqs +The epoch IS the wait identity, so `channel::Inner.{cur_wait,next_wait_seq}` +and `mutex`'s per-wait `seq` are subsumed: registrations become +`(pid, epoch)`, `TimerTarget::on_timeout(pid, epoch)`, wakers call +`unpark_at`. Sleep timers, io completions (`Blocking` + `FdReady`) and join +waiters stamped likewise. End-state invariant, auditable in one sentence: +**the only wildcard wake is `request_stop`.** + +### 3. `select` — ready-index wait over multiple receivers +`select(&[&dyn Selectable]) -> usize`: register self in each arm's +`parked_receiver` (sequentially — the park protocol makes cross-arm atomicity +unnecessary), park once, wake, scan, return the first ready index; caller +`try_recv`s it. Sound as ready-index *because channels are single-receiver*: +nobody can steal between ready and recv. A closed arm counts as ready (its +`try_recv` reports the closure). Arms are scanned in order — priority order, +documented, like BEAM receive clauses; no fairness promise. No cancel pass +(see protocol above). Drop-guard deregistration on the unwind path only, to +keep tests' single-parked-receiver asserts honest. + +### 4. `select_timeout` +A `WaitTimeout` timer stamped with the select's epoch, whose target is a +stateless `unpark_at`. The woken selector classifies the wake from state +alone: some arm ready → that arm; none ready → with precise wakes the timer +is the only remaining stamped waker → `None`. No flag, no shared waiter +struct, no registration to leak. The timer entry expires as a stale-epoch +no-op when an arm wins, per the no-cancellation convention in `timer.rs`. + +### 5. Docs + integration proof +README module table, gotchas rewrite (the "No `select`" bullet becomes the +consuming-wake invariant), and an integration test of the motivating +pattern: a server selecting over its inbox and a monitor `Down` channel. + +Unblocked but deliberately out of scope: `gen_server::handle_info` (now an +API-design question, not a runtime question — see Later). + +--- + ## Later ### Tunable scheduler idle policy (RFC 004) @@ -175,9 +248,10 @@ Revisit with a segmented slab (array of `AtomicPtr`, doubling segment sizes, append-only) once the cap is actually hit. Do not let it calcify. ### gen_server: handle_info -Needs a cross-channel mailbox merge (call/cast inbox + out-of-band signals) — -the still-unmade decision between a unified per-actor mailbox and per-channel -`recv_match` composition. +Runtime prerequisite landed in v0.7 (`select`). What remains is API design: +the `Info` message type, who owns the extra channels, whether monitor `Down`s +auto-forward into the server loop. Decide against real consumers, not +speculatively. ### IO fd hygiene on actor death Pre-existing v0.2 TODO in `io.rs`: fds registered with epoll for a dead actor