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.)
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::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.
///
@@ -196,6 +198,27 @@ impl<G: GenServer> ServerRef<G> {
.send(Envelope::Cast(request))
.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
@@ -274,6 +297,22 @@ impl<G: GenServer> ServerBuilder<G> {
/// lifetime is governed by its refs, not by joining, so the backing join
/// handle is dropped.
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 ServerBuilder { state, infos, supervisor } = self;
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
/// [`ServerRef`]. Shorthand for `ServerBuilder::new(state).start()`.
pub fn start<G: GenServer>(state: G) -> ServerRef<G> {