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:
+168
-2
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user