mailbox: typed-path producers, by-name gen_servers, root-exit teardown (RFC 014)

Implements the new addressing surface the examples in examples/ specify:

- spawn_addr::<A>(FnOnce(Receiver<A::Msg>)) -> Pid<A>: the typed-path
  producer. Makes the inbox, spawns the body with its receiver, and publishes
  the sender from the PARENT side (registry::install_for) before returning the
  pid, so an immediate send_to always resolves (no race on the body installing
  itself). Detached, like ServerBuilder::start.

- lookup_as / pick_as / members_as: re-type an erased pid as Pid<A> over the
  heterogeneous registry/pg stores, sharing one helper (pid::assert_type).
  Unchecked but sound: routing is by message TypeId, so a wrong A degrades to
  SendError::NoChannel on the next send, never a misdelivery.

- dispatch::<A>(group, msg) -> Result<Pid<A>, SendError<A::Msg>>: pick_as +
  send_to, with the message handed back on failure. New SendError::NoMember
  variant for the empty/all-dead group case.

- gen_server naming: ServerName<G> backed internally by the registry's existing
  typed-channel store keyed by TypeId::of::<Envelope<G>>() — Envelope stays
  private, no separate directory. Type-state NamedServerBuilder<G> keeps the
  current infallible ServerBuilder::start() untouched; its start() is fallible
  (parent-side register, NameTaken). Free call/cast/whereis_server resolve per
  use. ServerRef::shutdown (+ free shutdown) is the sys-style synchronous stop.

- root-exit teardown: the run's initial actor is recorded as root; when it
  finalizes it flags root_exited, and the scheduler's idle verdict then stops
  the parked-forever remainder (a one-shot RootDrain sweep). Deferred to the
  idle point on purpose: the run queue drains first, so actors with queued work
  finish rather than unwinding on the stop. This lets a named-server daemon (or
  any pinned actor) wind the run down instead of hanging on live_actors > 0.

request_stop is refactored to a request_stop_inner core so the sweep can drive
it from inside the runtime without re-borrowing the thread-local.

Lib + tests + examples build warning-free; full suite green.
This commit is contained in:
smarm-agent
2026-06-18 11:02:27 +00:00
parent 4c56938f0b
commit a866e34b52
7 changed files with 440 additions and 24 deletions
+168 -2
View File
@@ -50,9 +50,11 @@
//! see `handle_down` — because monitors are inherently created at runtime.) //! see `handle_down` — because monitors are inherently created at runtime.)
use crate::channel::{channel, select, Receiver, Selectable, Sender}; use crate::channel::{channel, select, Receiver, Selectable, Sender};
use crate::monitor::{Down, Monitor}; use crate::monitor::{demonitor, monitor, Down, Monitor};
use crate::pid::Pid; use crate::pid::Pid;
use crate::scheduler::{spawn, spawn_under}; use crate::registry::{register_with, resolve_named_sender, RegisterError};
use crate::scheduler::{request_stop, spawn, spawn_under};
use std::marker::PhantomData;
/// Behaviour for a gen_server: a state value plus call/cast handlers. /// Behaviour for a gen_server: a state value plus call/cast handlers.
/// ///
@@ -196,6 +198,27 @@ impl<G: GenServer> ServerRef<G> {
.send(Envelope::Cast(request)) .send(Envelope::Cast(request))
.map_err(|_| CastError::ServerDown) .map_err(|_| CastError::ServerDown)
} }
/// Ask the server to terminate and block until it has — the `sys`-style
/// stop (cf. Erlang's `gen_server:stop/1`). Sends a cooperative stop to the
/// server actor and waits for its `Down`, so [`GenServer::terminate`] has
/// run by the time this returns (it fires from the loop's drop guard on the
/// stop unwind). Returns immediately if the server was already gone.
///
/// This is the explicit teardown for a server pinned alive by a registered
/// [`ServerName`] (whose stored sender means dropping every external
/// [`ServerRef`] no longer closes the inbox). Best-effort like all
/// cooperative cancellation: a server wedged in a tight loop with no
/// observation point cannot be stopped. Panics if called outside
/// `Runtime::run()`.
pub fn shutdown(&self) {
let mon = monitor(self.pid);
request_stop(self.pid);
// The Down lands when the server finalizes; an already-dead target makes
// `monitor` deliver NoProc immediately, so this never blocks forever.
let _ = mon.rx.recv();
demonitor(&mon);
}
} }
/// The server loop's runtime hook, passed to [`GenServer::init`]. Currently /// The server loop's runtime hook, passed to [`GenServer::init`]. Currently
@@ -274,6 +297,22 @@ impl<G: GenServer> ServerBuilder<G> {
/// lifetime is governed by its refs, not by joining, so the backing join /// lifetime is governed by its refs, not by joining, so the backing join
/// handle is dropped. /// handle is dropped.
pub fn start(self) -> ServerRef<G> { pub fn start(self) -> ServerRef<G> {
self.spawn_server()
}
/// Bind the server to a durable [`ServerName`] as it starts. Switches to the
/// fallible [`NamedServerBuilder::start`] (the name may already be held by a
/// live server). Consumes the builder, carrying its `with_info` / `under`
/// configuration through.
pub fn named(self, name: ServerName<G>) -> NamedServerBuilder<G> {
NamedServerBuilder { builder: self, name: name.as_str() }
}
/// The shared spawn body behind [`start`](Self::start) and
/// [`NamedServerBuilder::start`]: make the inbox, spawn the loop, return the
/// ref. (The named path additionally publishes the inbox sender under the
/// name before returning.)
fn spawn_server(self) -> ServerRef<G> {
let (tx, rx) = channel::<Envelope<G>>(); let (tx, rx) = channel::<Envelope<G>>();
let ServerBuilder { state, infos, supervisor } = self; let ServerBuilder { state, infos, supervisor } = self;
let handle = match supervisor { let handle = match supervisor {
@@ -284,6 +323,133 @@ impl<G: GenServer> ServerBuilder<G> {
} }
} }
/// A durable name typed by the *server* (RFC 014): a gen_server is multi-message
/// (call / cast over one inbox), so it is addressed by `G` rather than by a
/// single message type. By-name [`call`] / [`cast`] check the request against
/// `G`'s `Call` / `Cast` / `Reply`. Declared as a constant and shared freely:
///
/// ```ignore
/// const COUNTER: ServerName<Counter> = ServerName::new("counter");
/// ```
///
/// Under the hood the server's inbox is published into the registry as a
/// `Sender<Envelope<G>>` keyed by its message `TypeId` — the same channel store
/// every other name uses — so naming needs no separate directory. `Envelope`
/// stays private: only `ServerName<G>` opens that door.
pub struct ServerName<G> {
name: &'static str,
_marker: PhantomData<fn() -> G>,
}
impl<G> ServerName<G> {
/// Bind a static string as a server name. `const`, so names live as
/// associated constants at call sites.
#[inline]
pub const fn new(name: &'static str) -> Self {
Self { name, _marker: PhantomData }
}
/// The underlying registry key.
#[inline]
pub const fn as_str(self) -> &'static str {
self.name
}
}
impl<G> Copy for ServerName<G> {}
impl<G> Clone for ServerName<G> {
fn clone(&self) -> Self {
*self
}
}
/// A [`ServerBuilder`] that will bind a [`ServerName`] as it starts. Reached via
/// [`ServerBuilder::named`]; its [`start`](Self::start) is fallible because the
/// name may already be held by a live server. `with_info` / `under` stay
/// available so configuration can come before or after `named`.
pub struct NamedServerBuilder<G: GenServer> {
builder: ServerBuilder<G>,
name: &'static str,
}
impl<G: GenServer> NamedServerBuilder<G> {
/// Add an out-of-band info channel (see [`ServerBuilder::with_info`]).
pub fn with_info(mut self, rx: Receiver<G::Info>) -> Self {
self.builder = self.builder.with_info(rx);
self
}
/// Spawn under an explicit supervisor (see [`ServerBuilder::under`]).
pub fn under(mut self, supervisor: Pid) -> Self {
self.builder = self.builder.under(supervisor);
self
}
/// Spawn the server and bind its name in one step. Fallible: returns
/// [`RegisterError::NameTaken`] if the name is already held by a different
/// live server.
///
/// The inbox sender is published under the name **from the parent side,
/// before this returns**, so a by-name `call` / `cast` resolves the instant
/// `start()` returns — no race with the server body. On a name clash the
/// just-spawned server is wound down (its only ref is dropped, closing the
/// inbox), so a failed bind leaks no actor.
pub fn start(self) -> Result<ServerRef<G>, RegisterError> {
let NamedServerBuilder { builder, name } = self;
let server = builder.spawn_server();
match register_with::<Envelope<G>>(server.pid, name, server.tx.clone()) {
Ok(()) => Ok(server),
Err(e) => {
drop(server); // inbox closes → loop exits gracefully
Err(e)
}
}
}
}
/// Resolve a [`ServerName`] to a [`ServerRef`] when you want a handle to hold or
/// pass on rather than resolve per call. Rebuilds the ref from the registry's
/// stored inbox sender; `None` if no live server holds the name.
///
/// Panics if called outside `Runtime::run()`.
pub fn whereis_server<G: GenServer>(name: ServerName<G>) -> Option<ServerRef<G>> {
resolve_named_sender::<Envelope<G>>(name.as_str()).map(|(pid, tx)| ServerRef { tx, pid })
}
/// Synchronous request-reply to the server currently registered under `name`,
/// resolving through the registry on every call (so a server restarted under
/// the same name is reached transparently). [`CallError::ServerDown`] if no live
/// server holds the name, or if it dies before replying.
///
/// Panics if called outside `Runtime::run()`.
pub fn call<G: GenServer>(name: ServerName<G>, request: G::Call) -> Result<G::Reply, CallError> {
match whereis_server(name) {
Some(server) => server.call(request),
None => Err(CallError::ServerDown),
}
}
/// Fire-and-forget to the server registered under `name`, resolving per cast.
/// [`CastError::ServerDown`] if no live server holds the name.
///
/// Panics if called outside `Runtime::run()`.
pub fn cast<G: GenServer>(name: ServerName<G>, request: G::Cast) -> Result<(), CastError> {
match whereis_server(name) {
Some(server) => server.cast(request),
None => Err(CastError::ServerDown),
}
}
/// Terminate the server registered under `name` and block until it is down (see
/// [`ServerRef::shutdown`]). A no-op if no live server holds the name.
///
/// Panics if called outside `Runtime::run()`.
pub fn shutdown<G: GenServer>(name: ServerName<G>) {
if let Some(server) = whereis_server(name) {
server.shutdown();
}
}
/// Spawn `state` as a server under the current actor (via [`spawn`]). Returns a /// Spawn `state` as a server under the current actor (via [`spawn`]). Returns a
/// [`ServerRef`]. Shorthand for `ServerBuilder::new(state).start()`. /// [`ServerRef`]. Shorthand for `ServerBuilder::new(state).start()`.
pub fn start<G: GenServer>(state: G) -> ServerRef<G> { pub fn start<G: GenServer>(state: G) -> ServerRef<G> {
+8 -4
View File
@@ -50,18 +50,22 @@ pub use channel::{
channel, select, select_timeout, try_select, try_select_timeout, Receiver, RecvError, channel, select, select_timeout, try_select, try_select_timeout, Receiver, RecvError,
RecvTimeoutError, Selectable, Sender, RecvTimeoutError, Selectable, Sender,
}; };
pub use gen_server::{CallError, CallTimeoutError, CastError, GenServer, ServerBuilder, ServerCtx, ServerRef, Watcher}; pub use gen_server::{
call, cast, shutdown, whereis_server, CallError, CallTimeoutError, CastError, GenServer,
NamedServerBuilder, ServerBuilder, ServerCtx, ServerName, ServerRef, Watcher,
};
pub use link::{link, trap_exit, unlink, ExitSignal}; pub use link::{link, trap_exit, unlink, ExitSignal};
pub use monitor::{demonitor, monitor, Down, DownReason, Monitor, MonitorId}; pub use monitor::{demonitor, monitor, Down, DownReason, Monitor, MonitorId};
pub use mutex::{LockTimeout, Mutex, MutexGuard}; pub use mutex::{LockTimeout, Mutex, MutexGuard};
pub use pid::{Addressable, Erased, Name, Pid, RawPid}; pub use pid::{Addressable, Erased, Name, Pid, RawPid};
pub use pg::{join, leave, members, pick, Incarnation, Member, NodeId}; pub use pg::{dispatch, join, leave, members, members_as, pick, pick_as, Incarnation, Member, NodeId};
pub use registry::{ pub use registry::{
install, register, send, send_dyn, send_to, unregister, whereis, RegisterError, SendError, install, lookup_as, register, send, send_dyn, send_to, unregister, whereis, RegisterError,
SendError,
}; };
pub use runtime::{init, Config, Runtime}; pub use runtime::{init, Config, Runtime};
pub use scheduler::{ pub use scheduler::{
block_on_io, request_stop, run, self_pid, sleep, spawn, spawn_under, wait_readable, block_on_io, request_stop, run, self_pid, sleep, spawn, spawn_addr, spawn_under, wait_readable,
wait_readable_timeout, wait_writable, wait_writable_timeout, yield_now, FdArm, JoinError, wait_readable_timeout, wait_writable, wait_writable_timeout, yield_now, FdArm, JoinError,
JoinHandle, JoinHandle,
}; };
+39 -1
View File
@@ -97,7 +97,8 @@
//! discipline as `demonitor`). //! discipline as `demonitor`).
use crate::monitor::{demonitor, monitor, Monitor}; use crate::monitor::{demonitor, monitor, Monitor};
use crate::pid::Pid; use crate::pid::{assert_type, Addressable, Pid};
use crate::registry::{send_to, SendError};
use crate::scheduler::with_runtime; use crate::scheduler::with_runtime;
use std::collections::HashMap; use std::collections::HashMap;
@@ -408,6 +409,43 @@ pub fn pick(group: &str) -> Option<Pid> {
picked picked
} }
/// Typed `pick`: one live member of `group` as a [`Pid<A>`] (RFC 014 §4.4).
/// For a homogeneous pool every member is an `A`, so the picked member comes
/// back typed and dispatch is an ordinary compile-checked [`send_to`] rather
/// than the [`send_dyn`](crate::send_dyn) escape hatch. Re-types via the
/// unchecked [`assert_type`] primitive — a wrong `A` degrades to
/// [`SendError::NoChannel`] on the next send, never a misdelivery.
///
/// Panics if called outside `Runtime::run()`.
pub fn pick_as<A: Addressable>(group: &str) -> Option<Pid<A>> {
pick(group).map(assert_type::<A>)
}
/// Typed `members`: every live member of `group` as a [`Pid<A>`], same
/// unchecked re-type as [`pick_as`]. Fan-out stays compile-checked end to end.
///
/// Panics if called outside `Runtime::run()`.
pub fn members_as<A: Addressable>(group: &str) -> Vec<Pid<A>> {
members(group).into_iter().map(assert_type::<A>).collect()
}
/// Pick a live member of `group` and send it `msg` in one step, returning the
/// member it reached on success. The pick-a-live-member-and-send combinator
/// over [`pick_as`] + [`send_to`].
///
/// Errors hand `msg` back undelivered: [`SendError::NoMember`] if the pool is
/// empty (or all-dead), otherwise whatever the underlying [`send_to`] returns
/// (e.g. the picked member died in the window between pick and send →
/// [`SendError::Dead`]).
///
/// Panics if called outside `Runtime::run()`.
pub fn dispatch<A: Addressable>(group: &str, msg: A::Msg) -> Result<Pid<A>, SendError<A::Msg>> {
match pick_as::<A>(group) {
Some(pid) => send_to::<A>(pid, msg).map(|()| pid),
None => Err(SendError::NoMember(msg)),
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+16
View File
@@ -119,6 +119,22 @@ impl<A> Pid<A> {
} }
} }
/// Re-type an erased pid as `Pid<A>` *unchecked* — the one shared primitive
/// behind `lookup_as` / `pick_as` / `members_as` (RFC 014 §4.4). The registry
/// and pg stores are heterogeneous in `A` (they hold actors of every type at
/// once), so resolving them yields a bare [`Pid`]; recovering the typed address
/// is necessarily an assertion the store cannot make for us.
///
/// **Not unsound.** Delivery routes on the message's [`TypeId`](std::any::TypeId)
/// (every send path keys the channel store by it), so a wrong `A` here does not
/// mis-deliver: the next [`send_to`](crate::send_to) finds no channel for
/// `A::Msg` on that actor and returns [`SendError::NoChannel`](crate::SendError::NoChannel).
/// A mistyped pid degrades to a clean send error, never a silent misroute.
#[inline]
pub(crate) fn assert_type<A>(pid: Pid) -> Pid<A> {
Pid::from_raw(pid.raw())
}
impl<A> Copy for Pid<A> {} impl<A> Copy for Pid<A> {}
impl<A> Clone for Pid<A> { impl<A> Clone for Pid<A> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
+81 -5
View File
@@ -96,8 +96,13 @@ pub enum SendError<M> {
Dead(M), Dead(M),
/// The actor is live but exposes no channel for this message type. /// The actor is live but exposes no channel for this message type.
NoChannel(M), NoChannel(M),
/// The actor's channel for this type is closed (its receiver is gone). /// The actor's channel for this message type is closed (its receiver is gone).
Closed(M), Closed(M),
/// No live member to deliver to — a [`dispatch`](crate::dispatch) over an
/// empty (or all-dead) process group. Group-addressed dispatch only; the
/// name-addressed counterpart is [`SendError::Unresolved`]. The message is
/// handed back undelivered.
NoMember(M),
} }
impl<M> SendError<M> { impl<M> SendError<M> {
@@ -107,7 +112,8 @@ impl<M> SendError<M> {
SendError::Unresolved(m) SendError::Unresolved(m)
| SendError::Dead(m) | SendError::Dead(m)
| SendError::NoChannel(m) | SendError::NoChannel(m)
| SendError::Closed(m) => m, | SendError::Closed(m)
| SendError::NoMember(m) => m,
} }
} }
@@ -117,6 +123,7 @@ impl<M> SendError<M> {
SendError::Dead(_) => "Dead", SendError::Dead(_) => "Dead",
SendError::NoChannel(_) => "NoChannel", SendError::NoChannel(_) => "NoChannel",
SendError::Closed(_) => "Closed", SendError::Closed(_) => "Closed",
SendError::NoMember(_) => "NoMember",
} }
} }
} }
@@ -134,6 +141,7 @@ impl<M> std::fmt::Display for SendError<M> {
SendError::Dead(_) => write!(f, "the addressed actor is no longer the live incarnation"), SendError::Dead(_) => write!(f, "the addressed actor is no longer the live incarnation"),
SendError::NoChannel(_) => write!(f, "actor has no channel for this message type"), SendError::NoChannel(_) => write!(f, "actor has no channel for this message type"),
SendError::Closed(_) => write!(f, "the actor's channel for this type is closed"), SendError::Closed(_) => write!(f, "the actor's channel for this type is closed"),
SendError::NoMember(_) => write!(f, "no live member in the process group"),
} }
} }
} }
@@ -212,8 +220,20 @@ fn live(inner: &crate::runtime::RuntimeInner, pid: Pid) -> bool {
/// live actor (a binding to a dead actor is pruned and the name treated as /// live actor (a binding to a dead actor is pruned and the name treated as
/// free). Panics if called outside `Runtime::run()`. /// free). Panics if called outside `Runtime::run()`.
pub fn register<M: Send + 'static>(name: Name<M>, tx: Sender<M>) -> Result<(), RegisterError> { pub fn register<M: Send + 'static>(name: Name<M>, tx: Sender<M>) -> Result<(), RegisterError> {
let me = self_pid(); register_with(self_pid(), name.as_str(), tx)
let key = name.as_str(); }
/// Bind `name` to `pid`'s mailbox and publish `tx` under `M`'s [`TypeId`], for
/// an explicit (already-live) actor rather than `self`. The shared core of
/// [`register`] (which passes `self_pid()`) and the parent-side server-name
/// bind in `gen_server` (which names a freshly spawned server before its body
/// has run, so the name resolves the instant `start()` returns). Same collision
/// rules and lock discipline as `register`.
pub(crate) fn register_with<M: Send + 'static>(
me: Pid,
key: &'static str,
tx: Sender<M>,
) -> Result<(), RegisterError> {
with_runtime(|inner| { with_runtime(|inner| {
let mut reg = inner.registry.lock(); let mut reg = inner.registry.lock();
if !live(inner, me) { if !live(inner, me) {
@@ -231,7 +251,7 @@ pub fn register<M: Send + 'static>(name: Name<M>, tx: Sender<M>) -> Result<(), R
} }
} }
} }
// Publish (or extend) my mailbox with this channel, then bind the name. // Publish (or extend) the mailbox with this channel, then bind the name.
publish_channel::<M>(&mut reg, me, tx); publish_channel::<M>(&mut reg, me, tx);
reg.by_name.insert(key, me.index()); reg.by_name.insert(key, me.index());
Ok(()) Ok(())
@@ -276,6 +296,23 @@ pub fn install<A: Addressable>(tx: Sender<A::Msg>) -> Pid<A> {
Pid::from_raw(me.raw()) Pid::from_raw(me.raw())
} }
/// Publish `tx` into `pid`'s mailbox under `M`'s [`TypeId`], for an explicit
/// (freshly minted, already-live) actor rather than `self`. The parent-side
/// half of [`spawn_addr`](crate::spawn_addr): the spawner makes the inbox and
/// publishes the sender here *before* handing back the `Pid<A>`, so an immediate
/// `send_to` on the returned pid always resolves — the address is live the
/// instant the caller holds it, with no dependence on the body having run yet.
///
/// Caller guarantees `pid` is the just-installed actor (Queued, this exact
/// incarnation); `publish_channel` replaces any stale leftover at the slot.
pub(crate) fn install_for<M: Send + 'static>(pid: Pid, tx: Sender<M>) {
with_runtime(|inner| {
let mut reg = inner.registry.lock();
debug_assert!(live(inner, pid), "install_for: pid must be a freshly spawned, live actor");
publish_channel::<M>(&mut reg, pid, tx);
});
}
/// The single actor currently registered under `name`, or `None` if unbound or /// The single actor currently registered under `name`, or `None` if unbound or
/// no longer live (the stale binding is pruned on the way out). /// no longer live (the stale binding is pruned on the way out).
pub fn whereis(name: &str) -> Option<Pid> { pub fn whereis(name: &str) -> Option<Pid> {
@@ -296,6 +333,45 @@ pub fn whereis(name: &str) -> Option<Pid> {
}) })
} }
/// Resolve `name` to a *typed* [`Pid<A>`] — the identity-bound counterpart of
/// [`whereis`] (RFC 014 §4.4). Recovers the compile-checked
/// [`send_to`](crate::send_to) path from a durable name: looks the name up,
/// then re-types the erased pid as `Pid<A>` via the unchecked
/// [`assert_type`](crate::pid::assert_type) primitive. A wrong `A` is not
/// unsound — it degrades to [`SendError::NoChannel`] on the next send (routing
/// is by message `TypeId`), never a misdelivery. `None` if unbound or dead.
///
/// Panics if called outside `Runtime::run()`.
pub fn lookup_as<A: Addressable>(name: &str) -> Option<Pid<A>> {
whereis(name).map(crate::pid::assert_type::<A>)
}
/// Resolve `name` to its actor's pid and a cloned `Sender<M>`, under the Leaf
/// lock (clone-under-lock, then release). The crate-internal building block for
/// `gen_server`'s by-name addressing: a named server publishes its inbox as a
/// `Sender<Envelope<G>>` (via [`register_with`]), and `whereis_server` / `call`
/// / `cast` recover that exact typed sender here to rebuild a `ServerRef<G>`.
/// `None` if unbound, dead (pruned on the way out), or holding no `M` channel.
pub(crate) fn resolve_named_sender<M: Send + 'static>(name: &str) -> Option<(Pid, Sender<M>)> {
with_runtime(|inner| {
let mut reg = inner.registry.lock();
let idx = *reg.by_name.get(name)?;
let pid = match reg.by_index.get(&idx).map(|m| m.pid) {
Some(pid) if live(inner, pid) => pid,
Some(_) => {
reg.prune(idx);
return None;
}
None => {
reg.by_name.remove(name);
return None;
}
};
let tx = reg.by_index.get(&idx).and_then(Mailbox::clone_sender::<M>)?;
Some((pid, tx))
})
}
/// Remove the binding for `name`, returning the actor it pointed at if still /// Remove the binding for `name`, returning the actor it pointed at if still
/// live. Only the *name* is freed; the actor's mailbox (and any other names for /// live. Only the *name* is freed; the actor's mailbox (and any other names for
/// it) remain. A binding to a dead actor is reported as `None`. /// it) remain. A binding to a dead actor is reported as `None`.
+82
View File
@@ -509,6 +509,19 @@ pub(crate) struct RuntimeInner {
/// Incremented in `spawn` before the enqueue; decremented at the very end /// Incremented in `spawn` before the enqueue; decremented at the very end
/// of `finalize_actor`, after every wakeup finalize produces. /// of `finalize_actor`, after every wakeup finalize produces.
pub(crate) live_actors: AtomicU32, pub(crate) live_actors: AtomicU32,
/// Packed `(index << 32 | generation)` of the run's root (initial) actor,
/// or `u64::MAX` (the ROOT_PID sentinel) before one is set. When this actor
/// finalizes it flags `root_exited`; the scheduler's idle verdict then
/// stops the remaining (parked-forever) actors. Set once per `run()`, right
/// after the initial spawn.
pub(crate) root_bits: AtomicU64,
/// Set when the root actor finalizes; read by the scheduler's idle verdict
/// to trigger the one-shot teardown sweep. Reset per `run()`.
pub(crate) root_exited: AtomicBool,
/// Guards the teardown sweep to fire at most once per run (a parked-forever
/// remainder that survives the sweep falls through to the normal idle wait
/// rather than busy-spinning). Reset per `run()`.
pub(crate) root_swept: AtomicBool,
/// Timer heap. Independent lock: never nested with any other. /// Timer heap. Independent lock: never nested with any other.
pub(crate) timers: Mutex<Timers>, pub(crate) timers: Mutex<Timers>,
/// IO subsystem. `None` between runs. Lock order: io before everything. /// IO subsystem. `None` between runs. Lock order: io before everything.
@@ -569,6 +582,9 @@ impl RuntimeInner {
slots, slots,
free: RawMutex::new(free), free: RawMutex::new(free),
live_actors: AtomicU32::new(0), live_actors: AtomicU32::new(0),
root_bits: AtomicU64::new(u64::MAX),
root_exited: AtomicBool::new(false),
root_swept: AtomicBool::new(false),
timers: Mutex::new(Timers::new()), timers: Mutex::new(Timers::new()),
io: Mutex::new(None), io: Mutex::new(None),
next_monitor_id: AtomicU64::new(0), next_monitor_id: AtomicU64::new(0),
@@ -597,6 +613,25 @@ impl RuntimeInner {
self.slots.get(pid.index() as usize) self.slots.get(pid.index() as usize)
} }
/// Record `pid` as this run's root actor. Called once per `run()`, right
/// after the initial spawn and before any scheduler thread starts, so no
/// finalize can observe the count before the root is set.
#[inline]
pub(crate) fn set_root(&self, pid: Pid) {
self.root_bits.store(Self::pack(pid), Ordering::Relaxed);
}
/// Is `pid` (index + generation) this run's root actor?
#[inline]
pub(crate) fn is_root(&self, pid: Pid) -> bool {
self.root_bits.load(Ordering::Relaxed) == Self::pack(pid)
}
#[inline]
fn pack(pid: Pid) -> u64 {
((pid.index() as u64) << 32) | pid.generation() as u64
}
/// Push to the run queue. Callers must have just transitioned the pid /// Push to the run queue. Callers must have just transitioned the pid
/// into `Queued` (spawn's publish, the unpark protocol, or the /// into `Queued` (spawn's publish, the unpark protocol, or the
/// scheduler's yield/notified-park return paths). /// scheduler's yield/notified-park return paths).
@@ -818,6 +853,11 @@ impl Runtime {
// requires a running runtime in the thread-local). // requires a running runtime in the thread-local).
RUNTIME.with(|r| *r.borrow_mut() = Some(self.inner.clone())); RUNTIME.with(|r| *r.borrow_mut() = Some(self.inner.clone()));
let initial_handle = crate::scheduler::spawn(f); let initial_handle = crate::scheduler::spawn(f);
// The initial actor is the run's root: when it exits, remaining actors
// are stopped so the run winds down (see finalize_actor / schedule_loop).
self.inner.root_exited.store(false, Ordering::Relaxed);
self.inner.root_swept.store(false, Ordering::Relaxed);
self.inner.set_root(initial_handle.pid());
// Launch N-1 extra scheduler threads. The calling thread is thread 0. // Launch N-1 extra scheduler threads. The calling thread is thread 0.
let mut os_threads = Vec::new(); let mut os_threads = Vec::new();
@@ -1117,6 +1157,16 @@ fn finalize_actor(inner: &Arc<RuntimeInner>, pid: Pid, outcome: Outcome) {
// Reclaim if no outstanding handles (re-verified inside). // Reclaim if no outstanding handles (re-verified inside).
reclaim_slot(inner, pid); reclaim_slot(inner, pid);
// Root-exit teardown is DEFERRED to the scheduler's idle verdict, not done
// here: stopping eagerly would cut off actors that still have queued work
// (they'd unwind on the stop before draining their mailbox). Flagging it
// instead lets the run queue drain naturally first; only the parked-forever
// remainder (e.g. a server pinned alive by a registered name) is then
// stopped, once nothing runnable is left. See `schedule_loop`.
if inner.is_root(pid) {
inner.root_exited.store(true, Ordering::Release);
}
// The decrement is LAST: every wakeup this finalize produced (joiners, // The decrement is LAST: every wakeup this finalize produced (joiners,
// monitor/trap sends, stop cascades) is enqueued before `live_actors` // monitor/trap sends, stop cascades) is enqueued before `live_actors`
// can be observed at its decremented value. See the termination note in // can be observed at its decremented value. See the termination note in
@@ -1125,6 +1175,19 @@ fn finalize_actor(inner: &Arc<RuntimeInner>, pid: Pid, outcome: Outcome) {
debug_assert!(prev >= 1, "live_actors underflow — double finalize"); debug_assert!(prev >= 1, "live_actors underflow — double finalize");
} }
/// Cooperatively stop every live actor — the root-exit teardown sweep, run from
/// `schedule_loop` once the run queue is empty after the root has exited. Each
/// [`request_stop_inner`](crate::scheduler::request_stop_inner) re-verifies the
/// target under its cold lock, so the racy per-slot generation read is safe: a
/// vacant, dead, or reused slot no-ops. The swept actors unpark, unwind at their
/// next observation point, and finalize, dropping `live_actors` to zero.
fn stop_live_actors(inner: &Arc<RuntimeInner>) {
for idx in 0..inner.slots.len() as u32 {
let pid = Pid::new(idx, inner.slots[idx as usize].generation());
crate::scheduler::request_stop_inner(inner, pid);
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// schedule_loop — runs on each scheduler OS thread // schedule_loop — runs on each scheduler OS thread
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -1220,6 +1283,9 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
Got(Pid), Got(Pid),
Idle { io_outstanding: u32, wake_fd: Option<std::os::fd::RawFd> }, Idle { io_outstanding: u32, wake_fd: Option<std::os::fd::RawFd> },
AllDone, AllDone,
/// Root has exited and nothing is runnable: stop the parked-forever
/// remainder, then re-pop. Fires at most once per run.
RootDrain,
} }
// 2a. RFC 005: drain this thread's wake slot before touching the // 2a. RFC 005: drain this thread's wake slot before touching the
@@ -1268,6 +1334,15 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
let live = inner.live_actors.load(Ordering::Acquire); let live = inner.live_actors.load(Ordering::Acquire);
if live == 0 && io_out == 0 { if live == 0 && io_out == 0 {
Pop::AllDone Pop::AllDone
} else if inner.root_exited.load(Ordering::Acquire)
&& !inner.root_swept.swap(true, Ordering::AcqRel)
{
// Root gone and nothing runnable — the live remainder
// are parked-forever daemons (Queued actors with pending
// work drained before the queue emptied). Stop them so
// the run can end. One-shot: a survivor falls through to
// the idle wait below on the next pass.
Pop::RootDrain
} else { } else {
Pop::Idle { io_outstanding: io_out, wake_fd: io_fd } Pop::Idle { io_outstanding: io_out, wake_fd: io_fd }
} }
@@ -1294,6 +1369,13 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
} }
return; return;
} }
Pop::RootDrain => {
// Root has exited and nothing is runnable: stop the
// parked-forever remainder, then loop back to re-pop the
// now-runnable (stopping) actors.
stop_live_actors(inner);
continue;
}
Pop::Idle { io_outstanding, wake_fd } => { Pop::Idle { io_outstanding, wake_fd } => {
// Something is still in flight. Sleep on the appropriate // Something is still in flight. Sleep on the appropriate
// source to avoid hammering the queue mutex; retry on wake. // source to avoid hammering the queue mutex; retry on wake.
+36 -2
View File
@@ -195,6 +195,33 @@ pub fn spawn_under<A>(supervisor: Pid<A>, f: impl FnOnce() + Send + 'static) ->
JoinHandle { pid, consumed: false } JoinHandle { pid, consumed: false }
} }
/// Spawn a typed, single-message actor and hand back its identity-bound
/// [`Pid<A>`] (RFC 014's typed-path producer). The runtime makes the actor's
/// inbox, hands the body its [`Receiver<A::Msg>`], installs the sender, and
/// returns the parent a `Pid<A>`.
///
/// The inbox is published from the parent side **before** the pid is returned
/// (see [`registry::install_for`](crate::registry::install_for)), so the address
/// is live the instant the caller holds it: an immediate
/// [`send_to`](crate::send_to) always resolves, never racing the body's first
/// instruction. The actor is detached — its lifetime is governed by its own
/// logic (an explicit stop message, or returning), like
/// [`ServerBuilder::start`](crate::ServerBuilder::start) — so the backing join
/// handle is dropped. Spawns under the current actor (via [`spawn`]).
///
/// Panics if called outside `Runtime::run()`.
pub fn spawn_addr<A: crate::pid::Addressable>(
body: impl FnOnce(crate::channel::Receiver<A::Msg>) + Send + 'static,
) -> Pid<A> {
let (tx, rx) = crate::channel::channel::<A::Msg>();
let handle = spawn(move || body(rx));
let pid = handle.pid();
// Publish the sender for `pid` before returning the typed address. `handle`
// drops at end of scope (detached).
crate::registry::install_for::<A::Msg>(pid, tx);
crate::pid::assert_type::<A>(pid)
}
use crate::context::init_actor_stack; use crate::context::init_actor_stack;
pub fn self_pid() -> Pid { pub fn self_pid() -> Pid {
@@ -290,7 +317,15 @@ pub(crate) fn retire_wait() {
/// if `pid` is already gone. /// if `pid` is already gone.
pub fn request_stop<A>(pid: Pid<A>) { pub fn request_stop<A>(pid: Pid<A>) {
let pid = pid.erase(); let pid = pid.erase();
let _ = try_with_runtime(|inner| { let _ = try_with_runtime(|inner| request_stop_inner(inner, pid));
}
/// The core of [`request_stop`], taking the runtime directly so it can be
/// driven from inside the runtime (e.g. the root-exit sweep in
/// `finalize_actor`) without re-borrowing the thread-local. Sets the stop flag
/// under the target's cold lock (generation re-verified there; a mismatch or a
/// slot with no live actor is a no-op) and wakes it.
pub(crate) fn request_stop_inner(inner: &RuntimeInner, pid: Pid) {
if let Some(slot) = inner.slot_at(pid) { if let Some(slot) = inner.slot_at(pid) {
{ {
let cold = slot.cold.lock(); let cold = slot.cold.lock();
@@ -304,7 +339,6 @@ pub fn request_stop<A>(pid: Pid<A>) {
} }
inner.unpark(pid); inner.unpark(pid);
} }
});
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------