diff --git a/README.md b/README.md index 7d53f5c..d46f2e6 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,15 @@ convenience wrapper around `runtime::init(Config::exact(1)).run(f)`. | `pid` | `(index, generation)` PIDs; stale handles are detectable, not silent | | `actor` | Trampoline + `catch_unwind` boundary at the actor entry point | | `scheduler` | Run queue, slot table, spawn/join, parking, idle path | -| `channel` | Unbounded MPSC channel; `recv` parks the actor | +| `channel` | Unbounded MPSC channel; `recv` parks the actor; `recv_timeout` bounds it | | `mutex` | `Mutex` with mandatory timeout; FIFO waiters; parks the green thread | | `timer` | Min-heap of `(deadline, reason)`; `Sleep` and `WaitTimeout` reasons | | `io` | `block_on_io` for blocking work; `wait_readable`/`wait_writable` + `read`/`write` via epoll | | `supervisor` | `Signal::Exit`/`Panic`/`Stopped` funnelled to a parent; `OneForOne`/`OneForAll`/`RestForOne` strategies + restart-intensity cap | | `monitor` | `monitor(pid)` → `Monitor { id, target, rx }`; one-shot `Down` via `rx`; `demonitor(&m)` tears one registration down; unidirectional death notice | | `link` | bidirectional `link`/`unlink`; abnormal death propagates (cooperative stop, or an `ExitSignal` message under `trap_exit`) | -| `gen_server` | `call` (sync request-reply) / `cast` (async) over one inbox; `ServerRef` + `init`/`terminate` hooks; server-down via channel closure | +| `gen_server` | `call`/`call_timeout` (sync request-reply) / `cast` (async) over one inbox; `ServerRef` + `init`/`terminate` hooks; server-down via channel closure | +| `registry` | `register`/`whereis`/`name_of`: name ↔ pid bimap; lazy generation-checked cleanup | ## Quick taste diff --git a/ROADMAP.md b/ROADMAP.md index 54ea03b..152b925 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -104,31 +104,51 @@ precondition. --- -## v0.6 — Up next +## v0.6 — Actor ergonomics ✅ DONE -### 1. Channel mutex migration (first change) -`channel::Inner` holds a `std::sync::Mutex`. Phase 2 surfaced a sharp -correctness issue: the `MutexGuard` is held with preemption **enabled**, so a -timeslice switch inside a channel critical section migrates the actor and -releases the pthread mutex from a different OS thread — UB (Linux futexes happen -to tolerate it, but it's not guaranteed). The Phase-1 `check_cancelled` gating -already removes the unwind source, so poison is safe today; this is about the -cross-thread release. +### 1. Channel mutex migration ✅ (`8ff6cf4`) +`channel::Inner` migrated from `std::sync::Mutex` to `RawMutex`, closing +the cross-thread pthread-release hole (guard held with preemption enabled → +timeslice switch → unlock from a different OS thread, UB) and removing poison. -Migrate to `RawMutex` (which disables preemption and is cross-thread-release -sound by construction). `recv_match` runs a user predicate under this lock — -keep it cheap/pure. +The strict leaf rule did not survive contact: finalize clones the +supervisor/trap senders and `monitor()` clones the Down sender, all under a +cold lock, and those senders *live in the slot* — the nesting is structural. +The check generalized to two classes: **Leaf → Channel**, at most one of +each, both violation directions debug-asserted at the acquisition site. +Channel critical sections call only the lock-free unpark protocol, so the +order is acyclic. `recv_match`'s user predicate still runs under the channel +lock — cheap/pure/channel-free, as documented. -### 2. Named registry -`register(name, pid)` / `whereis(name) -> Option` / `send_by_name`. -`HashMap` in `SharedState`, guarded by the shared lock. No -architecture changes; can land immediately after #1. +### 2. Named registry ✅ (`2c7cf0b`) +`register` / `whereis` / `unregister` + the inverse `name_of(pid)`: a +**bimap** (two HashMaps in exact inverse, debug-asserted) under one Leaf +`RawMutex` in `RuntimeInner` — the planned "HashMap in SharedState" predated +the v0.5 decomposition. Erlang semantics: one name per pid, one pid per +name, NameTaken only against a *live* holder. Cleanup is lazy: liveness +rides the generation-checked slot word (pids are never reused), dead +bindings behave as absent and are pruned on contact — no `finalize_actor` +hook, no Slot field, no reset-in-three-places exposure. -### 3. gen_server: call timeout -`call` with a deadline — monitor the server, wait reply-or-Down-or-deadline, -then `demonitor`+drop so a timed-out call leaks no registration. Requires a -per-`recv` deadline / `Signal::Timeout` primitive that doesn't exist yet. -(`handle_info` deferred — needs cross-channel mailbox merge; see Later.) +`send_by_name` was dropped from scope deliberately: smarm has no per-pid +mailbox, so there is nothing generic to send *to*. The registry yields a +`Pid` for monitor/link/request_stop and for routing user-owned channels. + +### 3. gen_server: call timeout ✅ (`134ff52`, `90b7040`) +Landed as two primitives, *without* the sketched monitor machinery: + +- `Receiver::recv_timeout(Duration)` — the per-`recv` deadline, on the + existing `timer::Reason::WaitTimeout`/`TimerTarget` machinery exactly as + `Mutex::lock_timeout` uses it (per-wait seq; stale heap entries no-op on + seq mismatch; message-first race resolution; `Disconnected` distinct from + `Timeout`). +- `ServerRef::call_timeout` = send + `recv_timeout` on the reply channel. + Server death is already observable there (reply sender drops as the server + unwinds), so monitor + demonitor-on-timeout had nothing left to guarantee: + no registration exists, so nothing can leak. Timed-out requests are still + handled; the late reply's send fails harmlessly (Erlang semantics). + +(`handle_info` still deferred — needs cross-channel mailbox merge; see Later.) --- @@ -189,6 +209,10 @@ hardware. Build + run full test suite on an aarch64 device; check cross-channel merge is the still-unmade decision (see v0.6 #3). - **Cooperative-only.** Preemption and cancellation both depend on the actor reaching `check!()`/yield/alloc/blocking points. +- **Lock order is Leaf → Channel, one of each at most** (debug-asserted in + `raw_mutex.rs`). Leaf = cold locks / free list / stack pool / registry, + mutual leaves. A channel lock may be taken under a Leaf (finalize/monitor + clone senders living in slots); nothing may be locked under a channel lock. - **Queue ops require preemption disabled.** A producer suspended mid-publish stalls every consumer — livelock. `with_runtime`, `with_shared`, and `RawMutex` guards all disable preemption for their span.