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:
+132
@@ -0,0 +1,132 @@
|
||||
//! Cooperative cancellation tests (roadmap item #1, the keystone).
|
||||
//!
|
||||
//! `request_stop(pid)` flags an actor for cancellation. The actor realizes the
|
||||
//! stop as a *controlled unwind*: at the next observation point (a `check!()`
|
||||
//! / allocation via `maybe_preempt`, or the wakeup side of any blocking park)
|
||||
//! a dedicated sentinel panic is raised, the trampoline's `catch_unwind` tears
|
||||
//! the stack down — running Drop guards — and reports `Outcome::Stopped`,
|
||||
//! distinct from a user `Panic`. Monitors see `DownReason::Stopped`.
|
||||
//!
|
||||
//! These cases are deterministic under the single-thread runtime: the parent
|
||||
//! runs until it parks, so the relative order of `request_stop`, the child
|
||||
//! reaching its observation point, and the monitor `Down` is fixed.
|
||||
|
||||
use smarm::{channel, monitor, request_stop, run, spawn, yield_now, DownReason};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Sets its flag when dropped — used to prove the cancellation unwind runs
|
||||
/// Drop guards rather than leaking the stack.
|
||||
struct DropFlag(Arc<AtomicBool>);
|
||||
impl Drop for DropFlag {
|
||||
fn drop(&mut self) {
|
||||
self.0.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn looping_actor_on_check_is_stopped() {
|
||||
let dropped = Arc::new(AtomicBool::new(false));
|
||||
let saw_stopped = Arc::new(AtomicBool::new(false));
|
||||
let (d, s) = (dropped.clone(), saw_stopped.clone());
|
||||
run(move || {
|
||||
let h = spawn(move || {
|
||||
let _g = DropFlag(d);
|
||||
// Tight loop whose only observation point is check!().
|
||||
loop {
|
||||
smarm::check!();
|
||||
}
|
||||
});
|
||||
let pid = h.pid();
|
||||
let down = monitor(pid);
|
||||
// Flag the stop before the child is ever resumed; it will observe the
|
||||
// flag once its check!() loop reaches the amortised preempt check.
|
||||
request_stop(pid);
|
||||
let dn = down.recv().expect("monitor channel closed before Down");
|
||||
assert_eq!(dn.pid, pid);
|
||||
if matches!(dn.reason, DownReason::Stopped) {
|
||||
s.store(true, Ordering::SeqCst);
|
||||
}
|
||||
let _ = h.join();
|
||||
});
|
||||
assert!(saw_stopped.load(Ordering::SeqCst), "expected DownReason::Stopped");
|
||||
assert!(dropped.load(Ordering::SeqCst), "Drop guard must run during the cancellation unwind");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parked_on_recv_actor_is_stopped() {
|
||||
let dropped = Arc::new(AtomicBool::new(false));
|
||||
let saw_stopped = Arc::new(AtomicBool::new(false));
|
||||
let (d, s) = (dropped.clone(), saw_stopped.clone());
|
||||
run(move || {
|
||||
let h = spawn(move || {
|
||||
let _g = DropFlag(d);
|
||||
let (tx, rx) = channel::<u8>();
|
||||
// Keep a sender alive so the channel stays open and recv() parks
|
||||
// indefinitely rather than returning Err.
|
||||
let _keep = tx;
|
||||
let _ = rx.recv(); // parks here until the stop unwinds us out
|
||||
});
|
||||
let pid = h.pid();
|
||||
let down = monitor(pid);
|
||||
// Let the child run and park in recv() before we request the stop.
|
||||
yield_now();
|
||||
request_stop(pid);
|
||||
let dn = down.recv().expect("monitor channel closed before Down");
|
||||
assert_eq!(dn.pid, pid);
|
||||
if matches!(dn.reason, DownReason::Stopped) {
|
||||
s.store(true, Ordering::SeqCst);
|
||||
}
|
||||
let _ = h.join();
|
||||
});
|
||||
assert!(saw_stopped.load(Ordering::SeqCst), "expected DownReason::Stopped");
|
||||
assert!(dropped.load(Ordering::SeqCst), "Drop guard must run on cancellation of a parked actor");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_check_no_alloc_loop_is_not_stopped() {
|
||||
// Documents the inherent gap: an actor that never reaches an observation
|
||||
// point (no check!(), no allocation, no blocking op) cannot be stopped —
|
||||
// the same limitation as preemption. Here the child runs a bounded,
|
||||
// allocation-free arithmetic loop, so the stop flagged before it runs is
|
||||
// silently never honored and the actor exits normally.
|
||||
let saw_exit = Arc::new(AtomicBool::new(false));
|
||||
let s = saw_exit.clone();
|
||||
run(move || {
|
||||
let h = spawn(|| {
|
||||
let mut x: u64 = 0;
|
||||
for i in 0..2_000_000u64 {
|
||||
x = x.wrapping_add(i ^ (x >> 1));
|
||||
}
|
||||
std::hint::black_box(x);
|
||||
});
|
||||
let pid = h.pid();
|
||||
let down = monitor(pid);
|
||||
request_stop(pid); // no observation point => ignored
|
||||
let dn = down.recv().expect("monitor channel closed before Down");
|
||||
if matches!(dn.reason, DownReason::Exit) {
|
||||
s.store(true, Ordering::SeqCst);
|
||||
}
|
||||
let _ = h.join();
|
||||
});
|
||||
assert!(
|
||||
saw_exit.load(Ordering::SeqCst),
|
||||
"a loop with no observation points must exit normally, never Stopped"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_on_stopped_actor_returns_ok() {
|
||||
// A cooperative stop carries no panic payload to propagate, so join()
|
||||
// reports Ok(()); the *fact* of the stop is observable via monitors
|
||||
// (DownReason::Stopped), which is the channel that carries termination
|
||||
// reason.
|
||||
run(|| {
|
||||
let h = spawn(|| loop {
|
||||
smarm::check!();
|
||||
});
|
||||
let pid = h.pid();
|
||||
request_stop(pid);
|
||||
assert!(h.join().is_ok(), "join on a stopped actor returns Ok(())");
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user