feat(runtime): phase 3 — pluggable run queue (rq-mutex / rq-mpmc / rq-striped)
src/run_queue.rs: the run queue extracted behind a compile-time-selected type alias; mutually-exclusive cargo features with compile_error! guards (zero or >1 selected). No runtime dispatch. All variants compile in every build so their unit tests always run; the feature only picks the alias. - rq-mutex (default): Mutex<VecDeque>, the control/baseline. - rq-mpmc: hand-rolled Vyukov bounded MPMC ring, per-cell sequence numbers, padded enqueue/dequeue counters. Strict FIFO, lock-free. - rq-striped: M Vyukov rings (M = thread_count rounded up to pow2), fetch-add ticket distribution, probe-from-home on push. Relaxed FIFO with stripe-bounded skew; Σcapacity ≈ 2×max_actors so the probe terminates. Capacity soundness: occupancy ≤ max_actors by the at-most-once-enqueued invariant + slab cap, rings sized ≥ that bound, so push is infallible; a full ring panics as a double-enqueue invariant violation rather than spin. Two contracts the extraction made explicit (documented + debug-asserted): - Queue ops require preemption disabled: a producer suspended between claiming a cell and publishing its sequence stalls every consumer behind it — livelock, since the suspended actor's own resume entry is behind the hole. Structurally guaranteed since the phase-2 with_runtime NoPreempt fix. - pop()==None is a snapshot, not a fence. Termination is counter-first: every queue entry's target stays Queued (hence live) until that entry is popped, so live==0 alone implies nothing actionable is or can be queued; argument rewritten at the schedule_loop site. SharedState and with_shared are deleted — nothing global is mutex-guarded on the run path under the ring variants. Validated: 22 suites green per variant (release for all three; debug with live asserts for all three), ring unit tests (FIFO, lap wraparound, 4p/4c exactly-once, skewed drain) in every build, both compile_error! guards verified to fire.
This commit is contained in:
+17
-8
@@ -63,19 +63,28 @@ Make slot lookup lock-free and per-slot state independently mutable.
|
||||
&& queue empty && live == 0; decrement-last ordering documented as the
|
||||
correctness crux at the site.
|
||||
|
||||
## Phase 3 — Pluggable run queue + bench shootout
|
||||
- [ ] Extract `RunQueue` behind a `type RunQueue = ...` alias selected at
|
||||
compile time by mutually-exclusive features, with a `compile_error!`
|
||||
guard if zero or >1 is set. No runtime dispatch.
|
||||
## Phase 3 — Pluggable run queue ✅ DONE (shootout = phase 4 harness)
|
||||
- [x] `RunQueue` alias in `src/run_queue.rs`, compile-time selected,
|
||||
`compile_error!` guards for zero / >1 features. No runtime dispatch.
|
||||
All variants compile in every build (unit tests always run); the
|
||||
feature only picks the alias. Non-default variants:
|
||||
`--no-default-features --features rq-…`.
|
||||
- `rq-mutex` — `Mutex<VecDeque>`, the control/baseline.
|
||||
- `rq-mpmc` — single hand-rolled Vyukov bounded MPMC ring
|
||||
(per-cell seq numbers). Strict FIFO; "one hot cache line".
|
||||
- `rq-striped` — M Vyukov rings, fetch-add ticket distribution. Relaxed
|
||||
FIFO, reordering bounded by ~M. Predicted winner @20c.
|
||||
- [ ] Bounded rings are sound because the slab cap + at-most-once-enqueued
|
||||
invariant bound occupancy ≤ `max_actors`; size Σcapacity > `max_actors`
|
||||
so push is infallible (probe next stripe when full; terminates).
|
||||
- [ ] All hand-rolled, dependency-free (portability incl. possible embedded).
|
||||
- [x] Bounded rings sound via slab cap + at-most-once-enqueued (occupancy ≤
|
||||
`max_actors`); mpmc capacity = next_pow2(max_actors), striped
|
||||
Σcapacity ≈ 2×max_actors with probe-from-home-stripe. A full ring
|
||||
panics as an invariant violation (double enqueue), never spins.
|
||||
- [x] All hand-rolled, dependency-free.
|
||||
- [x] Two contracts surfaced by the extraction, documented + debug-asserted
|
||||
in `run_queue.rs`: queue ops require preemption disabled (a producer
|
||||
suspended mid-publish stalls every consumer behind its cell — livelock);
|
||||
and pop-None is a snapshot, not a fence — termination is counter-first
|
||||
(`live == 0` alone implies the queue holds nothing actionable; argument
|
||||
rewritten at the `schedule_loop` site).
|
||||
|
||||
## Phase 4 — Bench harness
|
||||
- [ ] Raw-structure microbench: N threads, push/pop throughput vs thread count,
|
||||
|
||||
Reference in New Issue
Block a user