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:
smarm-dev
2026-06-07 21:51:56 +00:00
parent 09236b8bf4
commit d9a6520a24
8 changed files with 301 additions and 20 deletions
+29 -2
View File
@@ -24,8 +24,22 @@ use std::panic;
pub enum Outcome {
Exit,
Panic(Box<dyn Any + Send>),
/// The actor was cooperatively cancelled via `request_stop`: a sentinel
/// panic unwound its stack (running Drop) and the trampoline recognised
/// the sentinel. Distinct from a user `Panic` — there is no payload to
/// propagate.
Stopped,
}
/// The payload of the sentinel panic raised at an observation point when an
/// actor has been flagged for cooperative cancellation. Zero-sized; its only
/// job is to be recognisable in the trampoline's `catch_unwind` so the
/// teardown is reported as `Outcome::Stopped` rather than `Outcome::Panic`.
///
/// User code that wraps its own `catch_unwind` can swallow this (cf. Erlang's
/// `catch`); the stop flag stays set, so the next observation point re-raises.
pub(crate) struct StopSentinel;
// Thread-locals that the scheduler writes immediately before `switch_to_actor`.
thread_local! {
/// The closure for the actor we're about to resume *for the first time*.
@@ -83,8 +97,14 @@ pub extern "C-unwind" fn trampoline() {
.expect("trampoline entered without a closure set");
let outcome = match panic::catch_unwind(panic::AssertUnwindSafe(b)) {
Ok(()) => Outcome::Exit,
Err(payload) => Outcome::Panic(payload),
Ok(()) => Outcome::Exit,
Err(payload) => {
if payload.is::<StopSentinel>() {
Outcome::Stopped
} else {
Outcome::Panic(payload)
}
}
};
LAST_OUTCOME.with(|r| *r.borrow_mut() = Some(outcome));
@@ -107,4 +127,11 @@ pub struct Actor {
pub sp: usize,
/// The PID of this actor's supervisor. Used to deliver `Signal` on death.
pub supervisor: Pid,
/// Cooperative-cancellation flag. `request_stop` sets it (and unparks a
/// parked actor); the actor observes it lock-free at its next observation
/// point — see `preempt::check_cancelled`. Shared via `Arc` so the running
/// actor can poll it without taking the shared mutex. Lives on the `Actor`
/// (constructed fresh at spawn), so unlike a `Slot` field it needs no reset
/// in the slot-recycling paths.
pub stop: std::sync::Arc<std::sync::atomic::AtomicBool>,
}