feat(cancel): cooperative cancellation via sentinel unwind
request_stop(pid) sets a per-actor flag and wakes a parked target. The actor realizes the stop as a controlled unwind at its next observation point (check!()/alloc via maybe_preempt, or the wakeup side of any blocking park): a StopSentinel panic tears the stack down via the trampoline's existing catch_unwind, running Drop, and is reported as the new Outcome::Stopped (distinct from a user Panic). Death surface kept distinct from Exit: Signal::Stopped(pid) + DownReason::Stopped, so the supervisor await logic to come can tell a requested stop apart from a self-termination. Flag lives on Actor behind Arc<AtomicBool>; the scheduler resume path takes a raw pointer into it (no per-resume refcount traffic), keeping yield throughput at baseline.
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
|
||||
use std::alloc::{GlobalAlloc, Layout, System};
|
||||
use std::cell::Cell;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
pub const DEFAULT_ALLOC_INTERVAL: u32 = 128;
|
||||
pub const DEFAULT_TIMESLICE_CYCLES: u64 = 300_000; // ≈ 100µs on a 3 GHz CPU
|
||||
@@ -49,6 +50,54 @@ thread_local! {
|
||||
/// Per-thread copy of the configured timeslice, written once at
|
||||
/// scheduler-thread startup.
|
||||
static CONFIGURED_TIMESLICE_CYCLES: Cell<u64> = const { Cell::new(DEFAULT_TIMESLICE_CYCLES) };
|
||||
|
||||
/// Raw pointer to the on-CPU actor's cancellation flag, set by the
|
||||
/// scheduler on resume and reset to null when control returns to it.
|
||||
/// Null while no actor is running, so observation points are inert on the
|
||||
/// scheduler's own stack. A raw pointer (not a cloned `Arc`) keeps the
|
||||
/// resume path free of atomic ref-count traffic; see `check_cancelled` for
|
||||
/// the safety argument.
|
||||
static CURRENT_STOP: Cell<*const AtomicBool> = const { Cell::new(std::ptr::null()) };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cooperative-cancellation observation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Bind the on-CPU actor's stop flag. Called by the scheduler immediately
|
||||
/// before `switch_to_actor`. `flag` must point into the resuming actor's
|
||||
/// `Arc<AtomicBool>` heap box.
|
||||
pub(crate) fn set_current_stop(flag: *const AtomicBool) {
|
||||
CURRENT_STOP.with(|c| c.set(flag));
|
||||
}
|
||||
|
||||
/// Unbind the stop flag. Called by the scheduler on the return path from an
|
||||
/// actor, so the pointer never leaks into the scheduler loop or the next actor.
|
||||
pub(crate) fn clear_current_stop() {
|
||||
CURRENT_STOP.with(|c| c.set(std::ptr::null()));
|
||||
}
|
||||
|
||||
/// Observation point for cooperative cancellation. If the on-CPU actor has
|
||||
/// been flagged for stop, raise the sentinel panic so the trampoline's
|
||||
/// `catch_unwind` tears the stack down (running Drop) and reports
|
||||
/// `Outcome::Stopped`, distinct from a user `Panic`. A no-op (one TLS load,
|
||||
/// one relaxed atomic load) on the common path.
|
||||
///
|
||||
/// Called from `maybe_preempt` (amortised, the `check!()`/alloc path) and from
|
||||
/// the wakeup side of every blocking park (`park_current`/`yield_now`), which
|
||||
/// is past the prep-to-park window — so it can never lose a wakeup.
|
||||
#[inline]
|
||||
pub fn check_cancelled() {
|
||||
let p = CURRENT_STOP.with(|c| c.get());
|
||||
// SAFETY: `p` is either null (no actor on-CPU — the scheduler clears it on
|
||||
// every return) or a pointer into the on-CPU actor's `Arc<AtomicBool>`
|
||||
// heap box. That box outlives the run: an actor's slot is not finalized or
|
||||
// reclaimed while the actor is on-CPU (finalize runs only after it yields
|
||||
// back), and the Arc keeps the box alive at a fixed address even if the
|
||||
// slot table Vec reallocates underneath it.
|
||||
if !p.is_null() && unsafe { (*p).load(Ordering::Relaxed) } {
|
||||
std::panic::panic_any(crate::actor::StopSentinel);
|
||||
}
|
||||
}
|
||||
|
||||
/// Called once per scheduler thread at startup (before any actor runs).
|
||||
@@ -122,6 +171,10 @@ 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()) {
|
||||
let start = TIMESLICE_START.with(|s| s.get());
|
||||
if rdtsc().saturating_sub(start) > CONFIGURED_TIMESLICE_CYCLES.with(|t| t.get()) {
|
||||
|
||||
Reference in New Issue
Block a user