docs(roadmap): phase 2 done; record the thread-local-guard rule; sharpen the channel fast-follow rationale (cross-thread std MutexGuard release is UB)
This commit is contained in:
+34
-24
@@ -12,6 +12,12 @@ Guiding rules established this cycle:
|
|||||||
two slot locks at once). Any new lock states its position in this order.
|
two slot locks at once). Any new lock states its position in this order.
|
||||||
- **No unwind inside a runtime critical section.** The stop sentinel only
|
- **No unwind inside a runtime critical section.** The stop sentinel only
|
||||||
fires at lock-free observation points.
|
fires at lock-free observation points.
|
||||||
|
- **Never hold a thread-local guard across a possible switch point.** An
|
||||||
|
actor can be preempted at any allocation and resume on a different OS
|
||||||
|
thread; a live `RefCell` borrow (or std `MutexGuard`) then pairs its
|
||||||
|
acquire/release across two threads' locals — count underflow / UB.
|
||||||
|
`with_runtime`, `with_shared`, and every `RawMutex` guard disable
|
||||||
|
preemption for exactly this reason (phase 2 found this the hard way).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -30,32 +36,32 @@ Split independent concerns out of `SharedState` so the global lock guards less.
|
|||||||
|
|
||||||
`SharedState` now holds only: `slots`, `free_list`, `run_queue`, `root_pid`.
|
`SharedState` now holds only: `slots`, `free_list`, `run_queue`, `root_pid`.
|
||||||
|
|
||||||
## Phase 2 — Slot table split (NEXT, the big correctness step)
|
## Phase 2 — Slot table split ✅ DONE
|
||||||
Make slot lookup lock-free and per-slot state independently mutable.
|
Make slot lookup lock-free and per-slot state independently mutable.
|
||||||
- [ ] Fixed slab: `Box<[SlotCell; max_actors]>`, slots never move → stable
|
- [x] Fixed slab: `Box<[Slot]>`, slots never move → stable
|
||||||
addresses, lock-free index. **Assert on exhaustion** with a panic message
|
addresses, lock-free index. **Assert on exhaustion** with a panic message
|
||||||
naming `Config::max_actors(n)` as the fix. Default `max_actors = 16_384`.
|
naming `Config::max_actors(n)` as the fix. Default `max_actors = 16_384`.
|
||||||
(Mental note: must become unbounded or configurable-bounded later — see
|
(Mental note: must become unbounded or configurable-bounded later — see
|
||||||
"Deferred" below. Do not let the fixed cap calcify into an assumption.)
|
"Deferred" below. Do not let the fixed cap calcify into an assumption.)
|
||||||
- [ ] Per-slot `generation: AtomicU32` outside the lock for cheap stale-PID
|
- [x] Generation packed INTO the state word (better than the planned separate
|
||||||
rejection.
|
`AtomicU32`): one `AtomicU64 = (gen << 32) | state`, so the gen check is
|
||||||
- [ ] Per-slot CAS state machine replacing `state` + `pending_unpark`:
|
atomic with every transition — no ABA, no spurious unparks, by CAS.
|
||||||
`Parked / Queued / Running / RunningNotified / Done` in one `AtomicU8`.
|
- [x] Per-slot CAS state machine `Vacant/Queued/Running/RunningNotified/
|
||||||
`unpark` = CAS Parked→Queued (then push) or Running→RunningNotified;
|
Parked/Done` replacing `state` + `pending_unpark`. Also fixed a latent
|
||||||
park-return = CAS Running→Parked, else re-queue. Eliminates the
|
lost wakeup in the Blocking-IO completion path (result set for a
|
||||||
lost-wakeup bool by turning the race window into a state.
|
still-Running actor without a flag).
|
||||||
- [ ] `sp` → relaxed `AtomicUsize` (happens-before carried by queue
|
- [x] `sp` → relaxed `AtomicUsize`; stop flag + first-resume closure as
|
||||||
release-push / acquire-pop).
|
`AtomicPtr`s — resume path fully lock-free.
|
||||||
- [ ] Per-slot raw non-poisoning mutex (spin-then-futex, Linux; embedded-
|
- [x] Per-slot raw non-poisoning futex mutex (`src/raw_mutex.rs`) for the
|
||||||
friendlier than `std::sync::Mutex`) for the cold collections: `waiters`,
|
cold collections. Guard enters `NoPreempt`.
|
||||||
`monitors`, `links`, `supervisor_channel`, `outcome`, `pending_io_result`,
|
- [x] Free list → `RawMutex<Vec<u32>>` (a leaf lock). Treiber/striped only if
|
||||||
`outstanding_handles`. Guard enters `NoPreempt`.
|
the phase-4 spawn-storm bench shows contention — revisit then.
|
||||||
- [ ] Free list → its own structure (lock-free Treiber stack or striped).
|
- [x] `finalize_actor` link cascade locks peers one at a time; acyclicity
|
||||||
- [ ] `finalize_actor` link cascade: lock peers one at a time (leaf rule);
|
argument written at the site. `link()` registers target-first with the
|
||||||
write the acyclicity argument next to it.
|
race argument at the site.
|
||||||
- [ ] `live_actors` / `io_outstanding` → atomics; termination = both zero +
|
- [x] `live_actors` atomic; termination = io_out == 0 (read pre-queue-lock)
|
||||||
queue empty. Finalize decrements `live` strictly *after* all wakeups it
|
&& queue empty && live == 0; decrement-last ordering documented as the
|
||||||
produced are enqueued — document this ordering as the correctness crux.
|
correctness crux at the site.
|
||||||
|
|
||||||
## Phase 3 — Pluggable run queue + bench shootout
|
## Phase 3 — Pluggable run queue + bench shootout
|
||||||
- [ ] Extract `RunQueue` behind a `type RunQueue = ...` alias selected at
|
- [ ] Extract `RunQueue` behind a `type RunQueue = ...` alias selected at
|
||||||
@@ -92,9 +98,13 @@ Make slot lookup lock-free and per-slot state independently mutable.
|
|||||||
- **Channel mutex migration.** `channel::Inner<T>` is `Arc<Mutex<_>>` of the
|
- **Channel mutex migration.** `channel::Inner<T>` is `Arc<Mutex<_>>` of the
|
||||||
same poison class as the old shared lock; `recv_match` even runs a user
|
same poison class as the old shared lock; `recv_match` even runs a user
|
||||||
predicate under it. The Phase-1 `check_cancelled` gating already removes the
|
predicate under it. The Phase-1 `check_cancelled` gating already removes the
|
||||||
unwind *source* globally, so channels are safe today — but migrating the
|
unwind *source* globally, so channels are poison-safe today — but phase 2
|
||||||
channel to the raw non-poisoning mutex (Phase 2) is a clean follow-up rather
|
surfaced a second, sharper reason to migrate: the std `MutexGuard` is held
|
||||||
than bundled into the run-path rework.
|
with preemption ENABLED, so a timeslice switch inside a channel critical
|
||||||
|
section migrates the actor and unlocks the pthread mutex from a different
|
||||||
|
OS thread — technically UB (Linux futexes happen to tolerate it). The
|
||||||
|
`RawMutex` guard disables preemption and is cross-thread-release sound by
|
||||||
|
construction. Migrate as the first post-v0.5 change.
|
||||||
|
|
||||||
## Deferred / explicitly out of scope for v0.5
|
## Deferred / explicitly out of scope for v0.5
|
||||||
- **Unbounded / configurable-bounded actor count.** v0.5 ships a fixed slab
|
- **Unbounded / configurable-bounded actor count.** v0.5 ships a fixed slab
|
||||||
|
|||||||
Reference in New Issue
Block a user