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:
+45
-21
@@ -104,31 +104,51 @@ precondition.
|
||||
|
||||
---
|
||||
|
||||
## v0.6 — Up next
|
||||
## v0.6 — Actor ergonomics ✅ DONE
|
||||
|
||||
### 1. Channel mutex migration (first change)
|
||||
`channel::Inner<T>` 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<T>` 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<Pid>` / `send_by_name`.
|
||||
`HashMap<String, Pid>` 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.
|
||||
|
||||
Reference in New Issue
Block a user