feat(channel,runtime): off-runtime cross-thread wake for parked receivers and stop
Wakes issued from a non-scheduler OS thread were silent no-ops. Every off-runtime wake primitive (unpark, unpark_at, request_stop) reaches the runtime through the RUNTIME thread-local, which is unset on any foreign thread — so a send from a plain std::thread enqueued its message but never woke the parked receiver, and there was no way to drive a stop into a runtime from an application thread (e.g. an OS-signal handler). The former strands a parked recv forever; the latter is why a downstream server must poll a shutdown flag instead of parking on it. Generalize RFC 018's rule — a producer reaches the runtime through a Weak it holds — from the IO backend to channel senders and to a new handle: - A receiver captures a Weak<RuntimeInner> (provably live at that moment) alongside its (pid, epoch) when it parks. send() and the last-sender drop wake through scheduler::unpark_at_via, which takes the thread-local path when on a scheduler thread (preemption-gated, slot-eligible) and the captured Weak otherwise — the same cross-context wake the IO threads do. - Runtime::handle() returns a Send + Sync RuntimeHandle carrying that Weak; RuntimeHandle::request_stop drives a cooperative stop from any thread and is a no-op once the runtime is dropped. The in-runtime wake paths (recv/select timers) are unchanged; only the sites reachable from a foreign thread route through the Weak. RuntimeHandle exposes request_stop only: send-wake needs no user-facing handle, and off-runtime unpark is covered because request_stop drives unpark on the upgraded inner. tests/cross_thread_wake.rs: a foreign-thread send wakes a parked receiver; a foreign-thread request_stop wakes and stops a parked actor; a RuntimeHandle held across and beyond run never blocks all-done and degrades to a no-op.
This commit is contained in:
+54
-1
@@ -127,7 +127,7 @@ use crate::supervisor::Signal;
|
||||
use crate::timer::Timers;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::{Arc, Mutex, Weak};
|
||||
use std::thread;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1457,6 +1457,59 @@ impl Runtime {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A `Send + Sync` handle to this runtime, usable from any thread —
|
||||
/// including threads that are not smarm schedulers (an OS-signal handler
|
||||
/// thread, an external event source). Grab it *before* [`run`](Self::run)
|
||||
/// and hand it to e.g. a signal thread; that thread can then
|
||||
/// [`request_stop`](RuntimeHandle::request_stop) the runtime's top
|
||||
/// supervisor to drive an ordered shutdown from outside the runtime.
|
||||
///
|
||||
/// The in-runtime primitives ([`scheduler::request_stop`](crate::request_stop)
|
||||
/// and friends) reach the runtime through a thread-local that is unset on
|
||||
/// any non-scheduler thread, so they are silent no-ops off-runtime; this
|
||||
/// handle carries its own reference and closes that gap.
|
||||
pub fn handle(&self) -> RuntimeHandle {
|
||||
RuntimeHandle {
|
||||
inner: Arc::downgrade(&self.inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RuntimeHandle — off-runtime wake/stop
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A `Send + Sync` handle to a [`Runtime`], obtained from
|
||||
/// [`Runtime::handle`]. Lets a thread that is *not* a smarm scheduler thread
|
||||
/// drive a cooperative stop into the runtime — the off-runtime counterpart to
|
||||
/// [`scheduler::request_stop`](crate::request_stop).
|
||||
///
|
||||
/// Holds a [`Weak`] to the runtime, for the same reason the IO backend does
|
||||
/// (RFC 018): a lingering handle can never keep the runtime's slot table alive
|
||||
/// and can never block [`Runtime::run`] from finishing. Once the `Runtime` is
|
||||
/// dropped every method is a harmless no-op — the same end state as calling
|
||||
/// `request_stop` on an actor that has already exited.
|
||||
#[derive(Clone)]
|
||||
pub struct RuntimeHandle {
|
||||
inner: Weak<RuntimeInner>,
|
||||
}
|
||||
|
||||
impl RuntimeHandle {
|
||||
/// Ask `pid` to stop cooperatively, from any thread. The off-runtime
|
||||
/// equivalent of [`scheduler::request_stop`](crate::request_stop): it sets
|
||||
/// the target's stop flag and wakes it, so a parked actor unwinds at its
|
||||
/// next checkpoint exactly as it would for an in-runtime stop. A no-op if
|
||||
/// the runtime has been dropped, or if the actor has already exited.
|
||||
pub fn request_stop<A>(&self, pid: Pid<A>) {
|
||||
let pid = pid.erase();
|
||||
// Upgrade the Weak per call, like the IO backend does (io.rs): a live
|
||||
// runtime yields the inner and we drive the same stop the in-runtime
|
||||
// path would; a dropped runtime makes this a no-op.
|
||||
if let Some(inner) = self.inner.upgrade() {
|
||||
crate::scheduler::request_stop_inner(&inner, pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user