feat(runtime): phase 1 — peel timers/io/monitor-id out of SharedState; gate stop sentinel behind PREEMPTION_ENABLED

- next_monitor_id -> AtomicU64 on RuntimeInner
- timers -> own Mutex<Timers>; io -> own Mutex<Option<IoThread>> (lock order: io-before-shared)
- pending_closures Vec folded into Slot::pending_closure
- termination check reads io liveness before shared; ordering argument documented
- poison fix: check_cancelled() no longer fires while preemption is disabled,
  so a cancellation unwind can never poison a runtime/channel mutex
- regression test: tests/poison_stop.rs
- ROADMAP_v0.5.md added
This commit is contained in:
Claude
2026-06-09 18:56:17 +00:00
parent d908eb3f95
commit 3c7e26bc98
6 changed files with 303 additions and 108 deletions
+15 -4
View File
@@ -171,11 +171,22 @@ pub fn maybe_preempt() {
let n = c.get();
if n == 0 {
c.set(CONFIGURED_ALLOC_INTERVAL.with(|i| i.get()));
// Cooperative cancellation shares the amortised cadence with the
// timeslice check. Observe a pending stop first: if we are being
// cancelled there is no point yielding, we unwind instead.
check_cancelled();
if PREEMPTION_ENABLED.with(|e| e.get()) {
// Cooperative cancellation shares the amortised cadence with
// the timeslice check, and shares its gate: while preemption
// is disabled (`NoPreempt`, `with_shared`, channel critical
// sections) the stop sentinel must NOT be raised, because an
// allocation-triggered unwind inside a region holding a
// `std::sync::Mutex` would poison it — one `request_stop` at
// the wrong moment would then cascade `lock().unwrap()`
// panics through every later user of that lock. Observation
// is merely deferred to the next enabled allocation or the
// wakeup side of the next park/yield, both of which are
// lock-free points by construction.
//
// Observe a pending stop first: if we are being cancelled
// there is no point yielding, we unwind instead.
check_cancelled();
let start = TIMESLICE_START.with(|s| s.get());
if rdtsc().saturating_sub(start) > CONFIGURED_TIMESLICE_CYCLES.with(|t| t.get()) {
// SAFETY: reachable only inside an actor (the scheduler