docs(roadmap,readme): v0.6 complete — record outcomes and deviations
Marks the three v0.6 items done with their hashes and the load-bearing deviations: two-class lock order replacing the strict leaf rule, the registry as a bimap with name_of and without send_by_name (no per-pid mailbox to send to), and call_timeout built on recv_timeout instead of monitor machinery. Adds the Leaf -> Channel order to the standing invariants; README module table picks up recv_timeout, call_timeout, and the registry row.
This commit is contained in:
@@ -21,14 +21,15 @@ convenience wrapper around `runtime::init(Config::exact(1)).run(f)`.
|
|||||||
| `pid` | `(index, generation)` PIDs; stale handles are detectable, not silent |
|
| `pid` | `(index, generation)` PIDs; stale handles are detectable, not silent |
|
||||||
| `actor` | Trampoline + `catch_unwind` boundary at the actor entry point |
|
| `actor` | Trampoline + `catch_unwind` boundary at the actor entry point |
|
||||||
| `scheduler` | Run queue, slot table, spawn/join, parking, idle path |
|
| `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<T>` with mandatory timeout; FIFO waiters; parks the green thread |
|
| `mutex` | `Mutex<T>` with mandatory timeout; FIFO waiters; parks the green thread |
|
||||||
| `timer` | Min-heap of `(deadline, reason)`; `Sleep` and `WaitTimeout` reasons |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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`) |
|
| `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
|
## Quick taste
|
||||||
|
|
||||||
|
|||||||
+45
-21
@@ -104,31 +104,51 @@ precondition.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v0.6 — Up next
|
## v0.6 — Actor ergonomics ✅ DONE
|
||||||
|
|
||||||
### 1. Channel mutex migration (first change)
|
### 1. Channel mutex migration ✅ (`8ff6cf4`)
|
||||||
`channel::Inner<T>` holds a `std::sync::Mutex`. Phase 2 surfaced a sharp
|
`channel::Inner<T>` migrated from `std::sync::Mutex` to `RawMutex`, closing
|
||||||
correctness issue: the `MutexGuard` is held with preemption **enabled**, so a
|
the cross-thread pthread-release hole (guard held with preemption enabled →
|
||||||
timeslice switch inside a channel critical section migrates the actor and
|
timeslice switch → unlock from a different OS thread, UB) and removing poison.
|
||||||
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.
|
|
||||||
|
|
||||||
Migrate to `RawMutex` (which disables preemption and is cross-thread-release
|
The strict leaf rule did not survive contact: finalize clones the
|
||||||
sound by construction). `recv_match` runs a user predicate under this lock —
|
supervisor/trap senders and `monitor()` clones the Down sender, all under a
|
||||||
keep it cheap/pure.
|
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
|
### 2. Named registry ✅ (`2c7cf0b`)
|
||||||
`register(name, pid)` / `whereis(name) -> Option<Pid>` / `send_by_name`.
|
`register` / `whereis` / `unregister` + the inverse `name_of(pid)`: a
|
||||||
`HashMap<String, Pid>` in `SharedState`, guarded by the shared lock. No
|
**bimap** (two HashMaps in exact inverse, debug-asserted) under one Leaf
|
||||||
architecture changes; can land immediately after #1.
|
`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
|
`send_by_name` was dropped from scope deliberately: smarm has no per-pid
|
||||||
`call` with a deadline — monitor the server, wait reply-or-Down-or-deadline,
|
mailbox, so there is nothing generic to send *to*. The registry yields a
|
||||||
then `demonitor`+drop so a timed-out call leaks no registration. Requires a
|
`Pid` for monitor/link/request_stop and for routing user-owned channels.
|
||||||
per-`recv` deadline / `Signal::Timeout` primitive that doesn't exist yet.
|
|
||||||
(`handle_info` deferred — needs cross-channel mailbox merge; see Later.)
|
### 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).
|
cross-channel merge is the still-unmade decision (see v0.6 #3).
|
||||||
- **Cooperative-only.** Preemption and cancellation both depend on the actor
|
- **Cooperative-only.** Preemption and cancellation both depend on the actor
|
||||||
reaching `check!()`/yield/alloc/blocking points.
|
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
|
- **Queue ops require preemption disabled.** A producer suspended mid-publish
|
||||||
stalls every consumer — livelock. `with_runtime`, `with_shared`, and
|
stalls every consumer — livelock. `with_runtime`, `with_shared`, and
|
||||||
`RawMutex` guards all disable preemption for their span.
|
`RawMutex` guards all disable preemption for their span.
|
||||||
|
|||||||
Reference in New Issue
Block a user