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:
+24
-1
@@ -71,7 +71,7 @@ use crate::pid::{Name, Pid};
|
||||
use crate::runtime::{self, RuntimeInner, YieldIntent, RUNTIME};
|
||||
use crate::supervisor::Signal;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// with_runtime / try_with_runtime
|
||||
@@ -544,6 +544,29 @@ pub(crate) fn unpark_at(pid: Pid, epoch: u32) {
|
||||
let _ = try_with_runtime(|inner| inner.unpark_at(pid, epoch));
|
||||
}
|
||||
|
||||
// The current actor's runtime as a `Weak`, for a waker that must reach the
|
||||
// runtime from a foreign thread later. A channel captures this when its
|
||||
// receiver parks, so a cross-thread `send` can wake without the `RUNTIME`
|
||||
// thread-local (unset off a scheduler thread). Panics outside `Runtime::run()`,
|
||||
// the same contract as `begin_wait`.
|
||||
pub(crate) fn runtime_weak() -> Weak<RuntimeInner> {
|
||||
with_runtime(Arc::downgrade)
|
||||
}
|
||||
|
||||
// Epoch-matched wake of `pid` from a waker that may or may not be on a
|
||||
// scheduler thread. On a scheduler thread we take the thread-local path
|
||||
// (preemption-gated, slot-eligible); off one that path is a silent no-op, so
|
||||
// we reach the runtime through `rt` — the `Weak` the waker captured while it
|
||||
// was in-runtime. Mirrors the IO backend's cross-context wake (io.rs, RFC 018).
|
||||
pub(crate) fn unpark_at_via(pid: Pid, epoch: u32, rt: &Weak<RuntimeInner>) {
|
||||
if try_with_runtime(|inner| inner.unpark_at(pid, epoch)).is_some() {
|
||||
return;
|
||||
}
|
||||
if let Some(inner) = rt.upgrade() {
|
||||
inner.unpark_at(pid, epoch);
|
||||
}
|
||||
}
|
||||
|
||||
// Open a new wait for the current actor and return its wait identity
|
||||
// ("epoch"). Call once per wait, before registering with any waker. Lock-free,
|
||||
// so it's legal to call while already holding another internal lock.
|
||||
|
||||
Reference in New Issue
Block a user