docs(roadmap): v0.7 plan — select via epoch-stamped consuming wakes
This commit is contained in:
+77
-3
@@ -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<Segment>`, 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
|
||||
|
||||
Reference in New Issue
Block a user