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:
+29
-2
@@ -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>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user