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:
+28
-15
@@ -44,7 +44,7 @@ use crate::supervisor::Signal;
|
||||
use crate::timer::Timers;
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
@@ -542,6 +542,10 @@ fn finalize_actor(inner: &Arc<RuntimeInner>, pid: Pid, outcome: Outcome) {
|
||||
Signal::Panic(pid, Box::new(()) as Box<dyn std::any::Any + Send>),
|
||||
DownReason::Panic,
|
||||
),
|
||||
// Cooperative cancellation: kept distinct from a normal Exit so a
|
||||
// supervisor's await logic (roadmap #2) can tell "I stopped it" apart
|
||||
// from "it finished on its own".
|
||||
Outcome::Stopped => (Outcome::Stopped, Signal::Stopped(pid), DownReason::Stopped),
|
||||
};
|
||||
|
||||
let (waiters, supervisor_pid, monitors, recycled_stack) = inner.with_shared(|s| {
|
||||
@@ -730,7 +734,7 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot: usize) {
|
||||
// None with all_clear=false — idle, wait
|
||||
// ----------------------------------------------------------------
|
||||
enum PopResult {
|
||||
Work(Pid, usize, Option<Closure>),
|
||||
Work(Pid, usize, *const AtomicBool, Option<Closure>),
|
||||
Idle {
|
||||
next_deadline: Option<std::time::Instant>,
|
||||
io_outstanding: u32,
|
||||
@@ -738,23 +742,30 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot: usize) {
|
||||
},
|
||||
AllDone,
|
||||
}
|
||||
// SAFETY: the raw pointer is the actor's stack pointer, valid for the
|
||||
// lifetime of the actor's Slot. It is only dereferenced by
|
||||
// switch_to_actor() on this same thread immediately after this block.
|
||||
unsafe impl Send for PopResult {} // closure is Send; usize sp is plain data
|
||||
// SAFETY: the raw pointers are (a) the actor's stack pointer and (b) a
|
||||
// pointer into the actor's `Arc<AtomicBool>` stop flag, both valid for
|
||||
// the lifetime of the actor's Slot. They are only used by this same
|
||||
// thread immediately after this block (switch_to_actor / set_current_stop).
|
||||
unsafe impl Send for PopResult {} // closure is Send; usize sp + stop ptr are plain data
|
||||
|
||||
let pop = inner.with_shared(|s| {
|
||||
let len = s.run_queue.len() as u64;
|
||||
stats.run_queue_len.store(len, Ordering::Relaxed);
|
||||
|
||||
if let Some(pid) = s.run_queue.pop_front() {
|
||||
// Happy path: got a PID, grab SP and optional first closure.
|
||||
let sp = s.slot(pid)
|
||||
.and_then(|slot| slot.actor.as_ref().map(|a| a.sp));
|
||||
match sp {
|
||||
Some(sp) => {
|
||||
// Happy path: got a PID, grab SP, the stop-flag pointer, and
|
||||
// optional first closure. We hand out a raw pointer into the
|
||||
// actor's `Arc<AtomicBool>` rather than cloning the Arc: the
|
||||
// box stays alive and at a fixed address for as long as the
|
||||
// actor is on-CPU (it can't be finalized until it yields back),
|
||||
// so the scheduler's resume path pays no atomic ref-count cost.
|
||||
let info = s.slot(pid)
|
||||
.and_then(|slot| slot.actor.as_ref()
|
||||
.map(|a| (a.sp, std::sync::Arc::as_ptr(&a.stop))));
|
||||
match info {
|
||||
Some((sp, stop)) => {
|
||||
let closure = s.pop_pending_closure(pid);
|
||||
PopResult::Work(pid, sp, closure)
|
||||
PopResult::Work(pid, sp, stop, closure)
|
||||
}
|
||||
None => {
|
||||
// Stale PID (actor already finalized). Treat as idle
|
||||
@@ -792,10 +803,10 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot: usize) {
|
||||
}
|
||||
});
|
||||
|
||||
let (pid, sp, first_closure) = match pop {
|
||||
PopResult::Work(pid, sp, closure) => {
|
||||
let (pid, sp, stop_flag, first_closure) = match pop {
|
||||
PopResult::Work(pid, sp, stop, closure) => {
|
||||
crate::te!(crate::trace::Event::Dequeue(pid));
|
||||
(pid, sp, closure)
|
||||
(pid, sp, stop, closure)
|
||||
}
|
||||
PopResult::AllDone => return,
|
||||
PopResult::Idle { next_deadline, io_outstanding, wake_fd } => {
|
||||
@@ -841,6 +852,7 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot: usize) {
|
||||
|
||||
set_actor_sp(sp);
|
||||
set_current_pid(pid);
|
||||
crate::preempt::set_current_stop(stop_flag);
|
||||
reset_actor_done();
|
||||
YIELD_INTENT.with(|c| c.set(YieldIntent::Yield));
|
||||
crate::preempt::reset_timeslice();
|
||||
@@ -852,6 +864,7 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot: usize) {
|
||||
PREEMPTION_ENABLED.with(|c| c.set(false));
|
||||
stats.current_pid_index.store(u32::MAX, Ordering::Relaxed);
|
||||
clear_current_pid();
|
||||
crate::preempt::clear_current_stop();
|
||||
|
||||
let intent = YIELD_INTENT.with(|c| c.get());
|
||||
let new_sp = get_actor_sp();
|
||||
|
||||
Reference in New Issue
Block a user