docs(registry): user-facing rewrite of the name registry

Replace the 'what changed' diff-against-a-prior-design framing with a
plain explanation of what the registry is for (naming an actor so
others can find and message it by name) and a compiling doctest
(register/whereis/send/unregister). Cut all RFC/decision-number/bug-id
references and em-dashes; move type-erasure and locking-discipline
detail into an Implementation notes section for contributors.
This commit is contained in:
2026-07-24 08:40:26 +02:00
parent feda6517e5
commit 36a0a9832d
+242 -167
View File
@@ -1,55 +1,108 @@
//! Named mailbox registry — resolve a name (or pid) to a *messageable* actor.
//! Give an actor a name so other actors can find it and message it.
//! //!
//! ## What changed (RFC 014) //! Without the registry, the only way to reach an actor is to already be
//! holding its [`Pid`], usually because you spawned it yourself or someone
//! passed it to you. That is fine for a worker you just created, but it does
//! not work for a well-known service that arbitrary parts of your program
//! need to find independently, like a logger, a config store, or a
//! connection pool. The registry solves this: an actor claims a name once,
//! and from then on any other actor can look that name up, or send to it
//! directly, without ever having been handed a `Pid`.
//! //!
//! The old registry was a `name <-> pid` bimap: `whereis` handed back a `Pid` //! ```
//! you could not send to, because a pid is just `(index, generation)` with no //! use smarm::{channel, register, run, send, spawn, unregister, whereis, Name};
//! delivery endpoint. This rework makes resolution yield something messageable.
//! //!
//! Two facts shape the structure: //! const COUNTER: Name<u64> = Name::new("counter");
//! //!
//! 1. **A name resolves to a single actor.** Many actors under one label is //! run(|| {
//! what *process groups* (`pg`) are for; the registry is one-name-one-actor //! let (ready_tx, ready_rx) = channel::<()>();
//! (several names *may* point at the same actor). //! let (tx, rx) = channel::<u64>();
//! 2. **Channels are typed**, so an actor has no single untyped mailbox. An
//! actor instead owns a *set* of typed channels — one [`Sender`] per message
//! type it accepts. So the registry maps name/pid to a [`Mailbox`]: a small
//! structure holding that actor's pid plus all of its typed channels, keyed
//! by message [`TypeId`].
//! //!
//! Resolution is therefore: `name -> pid` (single actor) `-> Mailbox -> the //! let worker = spawn(move || {
//! channel for message type M`. A `Name<Cmd>` and a `Name<Admin>` on the *same* //! // Claim the name for this actor's inbox. Any actor holding
//! actor select *different* channels purely by their type parameter, so //! // `COUNTER` can now reach this one by name.
//! capability separation (RFC 014 §4.7) needs no extra machinery. //! register(COUNTER, tx).unwrap();
//! ready_tx.send(()).unwrap();
//! assert_eq!(rx.recv().unwrap(), 42);
//! });
//! //!
//! ## Type erasure is contained //! ready_rx.recv().unwrap(); // wait for the worker to register
//! //!
//! Each stored channel is a `Box<dyn Any + Send>` that is concretely a //! // Look the name up, or just send to it directly.
//! `Sender<M>`, filed under `TypeId::of::<M>()`. A resolve for `M` looks up //! assert_eq!(whereis("counter"), Some(worker.pid()));
//! that exact `TypeId` and downcasts to `Sender<M>` — keyed by the very type we //! send(COUNTER, 42).unwrap();
//! downcast to, so the downcast cannot fail on correct data; a failure is a
//! smarm bug, asserted in debug. The phantom `M` on [`Name`] re-imposes the
//! type at the call site, so callers never touch the erasure.
//! //!
//! ## Cleanup is lazy (prune-on-contact) //! worker.join().unwrap();
//! //!
//! As before, there is no `finalize` hook and no name field on the slot. Every //! // The name dies with the actor: nobody holds it anymore.
//! operation that touches a binding checks the target pid's liveness via the //! assert_eq!(whereis("counter"), None);
//! generation-checked slot word; a binding to a dead actor behaves as absent //! });
//! and is pruned on contact (its [`Mailbox`] and every name pointing at it are //! ```
//! dropped). The cost is a dead binding lingering until something looks at it;
//! the payoff is zero coupling to the actor lifecycle.
//! //!
//! ## Locking //! ## Names carry a message type
//! //!
//! One `RawMutex` (Leaf class) in `RuntimeInner`, exactly like the old //! A [`Name<M>`] is a plain string plus a type parameter `M`: the message
//! registry. The fold (name index *and* handles under the one lock) is what //! type that name expects to receive. [`Name::new`] is `const`, so the usual
//! keeps a name-addressed `send` on a single Leaf — `raw_mutex` panics on a //! pattern is a module-level constant like `COUNTER` above, shared by every
//! second Leaf acquired while one is held. The send path clones the `Sender` //! caller. The type parameter means a name is only ever sent the kind of
//! **under** the Leaf lock (a `Sender::clone` takes a Channel lock, permitted //! message it was declared for. If two different constants share the same
//! under a Leaf), then **releases** the Leaf and only *then* sends — a send can //! string but have different message types, they still address two
//! unpark a receiver, and wakeup-bearing work runs outside the Leaf. Order is //! independent channels on the same actor: registering both just gives that
//! **Leaf -> Channel**, as `pg`/`finalize`. //! actor two ways to be reached, one per message type. This is how you give
//! one actor a "public" channel and a separate, differently-typed "admin"
//! channel under related names, without inventing an enum to merge them.
//!
//! ## One actor per name, looked up fresh every time
//!
//! A name always points at exactly one actor at a time (contrast a *process
//! group*, from the [`pg`](crate::pg) module, which is one name mapping to
//! many actors). Unlike a plain [`Pid`], which names one specific actor
//! forever and stops working the moment that actor dies, a name is
//! re-resolved on every [`send`]: if the actor holding it dies and a new one
//! registers under the same name, the next `send` reaches the new holder
//! automatically. Use a name for a long-lived service whose exact identity
//! you do not want to track by hand; use a `Pid` when you already have one
//! and want to talk to that exact actor.
//!
//! ## Registration ends when the actor does
//!
//! There is no separate step to clean up a name when its actor exits: dying
//! is enough. The next operation that touches a dead binding (a [`whereis`],
//! a [`send`], or another actor's [`register`] of the same name) notices the
//! actor is gone and clears the stale entry as a side effect, so the name
//! becomes free again. [`unregister`] is only for a live actor voluntarily
//! giving up a name it no longer wants; nothing has to call it on the way
//! out.
//!
//! ## Implementation notes
//!
//! These details matter if you are working on smarm itself; they are not
//! part of the public contract.
//!
//! Internally, each live actor that has published at least one channel owns
//! a `Mailbox`: its pid plus a set of typed channels, keyed by the message
//! type's `TypeId`. A stored channel is a `Box<dyn Any + Send>` that
//! is concretely a `Sender<M>`; resolving for `M` looks up that exact
//! `TypeId` and downcasts, so the downcast cannot fail on correct data (a
//! failure would be a bug in the registry itself, checked in debug builds).
//! Registering a name therefore means: find or create the actor's mailbox,
//! insert the channel under its type, and point the name at the actor's pid.
//!
//! There is no callback when an actor exits. Every operation that touches a
//! binding checks the target pid's liveness directly against the scheduler's
//! slot table (which also tracks a generation counter, so a dead actor's
//! reused slot index is never mistaken for the same actor). A binding to a
//! dead actor is treated as absent and dropped right there. This keeps the
//! registry decoupled from actor teardown, at the cost of a dead binding
//! lingering until something happens to look at it.
//!
//! The whole registry (both the name index and the per-actor mailboxes) sits
//! behind one lock, which is what lets a name-addressed [`send`] resolve and
//! clone the target's sender in a single critical section. The sender is
//! cloned while that lock is held, then the lock is released before the
//! actual send, since delivering a message can wake a parked receiver and
//! that wakeup work should not run while the registry is locked.
use crate::channel::Sender; use crate::channel::Sender;
use crate::pid::{Addressable, Name, Pid}; use crate::pid::{Addressable, Name, Pid};
@@ -80,28 +133,33 @@ impl std::fmt::Display for RegisterError {
impl std::error::Error for RegisterError {} impl std::error::Error for RegisterError {}
/// Why a name-addressed [`send`] did not deliver. Carries the message back so /// Why a send did not deliver. Every variant carries the undelivered message
/// the caller never loses it (mirrors [`crate::channel::SendError`]). /// back, mirroring [`crate::channel::SendError`], so a failed send never
/// silently drops what you tried to send.
/// ///
/// `Debug`/`Display` are hand-written so neither demands `M: Debug` — the /// `Debug` and `Display` are hand-written so neither requires `M: Debug`,
/// payload is returned, not printed. /// since the payload is handed back to you, not printed.
pub enum SendError<M> { pub enum SendError<M> {
/// No live actor is currently registered under this name. Name-addressed /// No live actor is currently registered under this name. Returned only
/// [`send`] only; the pid-addressed counterpart is [`SendError::Dead`]. /// by name-addressed [`send`]; the pid-addressed counterpart of "nothing
/// there" is [`SendError::Dead`].
Unresolved(M), Unresolved(M),
/// The pid-addressed actor is no longer the live incarnation this pid names /// The actor this pid identifies has died, even if its slot has since
/// — it has died, even if its slot now holds a *different* actor (a direct /// been taken over by a different, live actor. A direct `Pid<A>` send
/// `Pid<A>` send never redirects; contrast name-addressed [`send`]). Pid /// never redirects to that new occupant; contrast name-addressed
/// paths ([`send_to`] / [`send_dyn`]) only. /// [`send`], which would reach it. Returned by the pid-addressed sends,
/// [`send_to`] and [`send_dyn`].
Dead(M), Dead(M),
/// The actor is live but exposes no channel for this message type. /// The actor is live but has not published a channel for this message
/// type.
NoChannel(M), NoChannel(M),
/// The actor's channel for this message type is closed (its receiver is gone). /// The actor's channel for this message type is closed (its receiver has
/// been dropped).
Closed(M), Closed(M),
/// No live member to deliver to — a [`dispatch`](crate::dispatch) over an /// No live member was available to deliver to: returned by
/// empty (or all-dead) process group. Group-addressed dispatch only; the /// [`dispatch`](crate::dispatch) when the target process group is empty
/// name-addressed counterpart is [`SendError::Unresolved`]. The message is /// or every member in it has died. The name-addressed counterpart of
/// handed back undelivered. /// this case is [`SendError::Unresolved`].
NoMember(M), NoMember(M),
} }
@@ -150,9 +208,9 @@ impl<M> std::error::Error for SendError<M> {}
/// A registry-stored channel, type-erased over its message type. The stored /// A registry-stored channel, type-erased over its message type. The stored
/// object must serve two readers: `clone_sender` (downcast back to the concrete /// object must serve two readers: `clone_sender` (downcast back to the concrete
/// `Sender<M>`) and the RFC 016 snapshot (queued length without knowing `M`). /// `Sender<M>`) and the runtime introspection snapshot (queued length without
/// A bare `Box<dyn Any>` gives the first but not the second, so we erase behind /// knowing `M`). A bare `Box<dyn Any>` gives the first but not the second, so
/// this small trait instead. /// we erase behind this small trait instead.
trait ErasedSender: Send { trait ErasedSender: Send {
fn as_any(&self) -> &dyn Any; fn as_any(&self) -> &dyn Any;
fn queued_len(&self) -> usize; fn queued_len(&self) -> usize;
@@ -169,7 +227,7 @@ impl<M: Send + 'static> ErasedSender for Sender<M> {
/// One typed channel of an actor, type-erased. Concretely a `Sender<M>` filed /// One typed channel of an actor, type-erased. Concretely a `Sender<M>` filed
/// under `TypeId::of::<M>()`; `msg_type` is `type_name::<M>()`, kept for /// under `TypeId::of::<M>()`; `msg_type` is `type_name::<M>()`, kept for
/// observers (RFC 014 §4.5) and as the debug cross-check on the downcast. /// observability tooling and as the debug cross-check on the downcast.
struct Channel { struct Channel {
sender: Box<dyn ErasedSender>, sender: Box<dyn ErasedSender>,
msg_type: &'static str, msg_type: &'static str,
@@ -204,7 +262,7 @@ impl Mailbox {
} }
} }
/// Per-actor registry view handed to RFC 016 introspection: registered names /// Per-actor registry view handed to runtime introspection: registered names
/// and summed mailbox depth, tagged with the mailbox's `pid` so a stale /// and summed mailbox depth, tagged with the mailbox's `pid` so a stale
/// incarnation can be filtered against the slab. Covers only *published* /// incarnation can be filtered against the slab. Covers only *published*
/// channels (`register` / `install` / `spawn_addr` / gen_server start); an /// channels (`register` / `install` / `spawn_addr` / gen_server start); an
@@ -219,17 +277,18 @@ pub(crate) struct MailboxInfo {
/// The directory. Invariant (held under the registry lock): every value in /// The directory. Invariant (held under the registry lock): every value in
/// `by_name` is the full [`Pid`] (index *and* generation) of an actor that /// `by_name` is the full [`Pid`] (index *and* generation) of an actor that
/// published a [`Mailbox`] into `by_index` at registration time. Stale entries /// published a [`Mailbox`] into `by_index` at registration time. Stale entries
/// (dead holders including holders whose slot has since been re-tenanted by /// (dead holders, including holders whose slot has since been re-tenanted by
/// a different actor) violate nothing they are pruned on contact, and the /// a different actor) violate nothing: they are pruned on contact, and the
/// generation makes "dead" decidable even after slot reuse. /// generation makes "dead" decidable even after slot reuse.
pub(crate) struct Registry { pub(crate) struct Registry {
/// `pid.index() -> the actor's mailbox`. The handle store. /// `pid.index() -> the actor's mailbox`. The handle store.
by_index: HashMap<u32, Mailbox>, by_index: HashMap<u32, Mailbox>,
/// `name -> holder pid`. Several names may map to one actor. The full pid /// `name -> holder pid`. Several names may map to one actor. The full pid
/// (not just the index) is load-bearing: an index alone cannot tell a dead /// (not just the index) is load-bearing: an index alone cannot tell a dead
/// holder from the live actor now tenanting its recycled slot, which made /// holder from the live actor now tenanting its recycled slot. Comparing
/// such a name read as live-held unresolvable *and* unregisterable — and /// only the index would make such a name read as live-held (unresolvable
/// would misdeliver to a same-typed tenant (soak20 signature 2). /// and unregisterable at once) and could misdeliver to whatever new,
/// same-typed actor now sits in that slot.
by_name: HashMap<&'static str, Pid>, by_name: HashMap<&'static str, Pid>,
} }
@@ -238,10 +297,10 @@ impl Registry {
Self { by_index: HashMap::new(), by_name: HashMap::new() } Self { by_index: HashMap::new(), by_name: HashMap::new() }
} }
/// Drop a dead holder's artifacts: every name bound to it, and its mailbox /// Drop a dead holder's artifacts: every name bound to it, and its
/// but only while the mailbox is still *its own*. A recycled slot's /// mailbox, but only while the mailbox is still *its own*. A recycled
/// mailbox belongs to the live tenant (publish replaces it wholesale on /// slot's mailbox belongs to the live tenant (publish replaces it
/// pid mismatch) and is left untouched. /// wholesale on pid mismatch) and is left untouched.
fn prune_holder(&mut self, holder: Pid) { fn prune_holder(&mut self, holder: Pid) {
self.by_name.retain(|_, p| *p != holder); self.by_name.retain(|_, p| *p != holder);
if self.by_index.get(&holder.index()).is_some_and(|mb| mb.pid == holder) { if self.by_index.get(&holder.index()).is_some_and(|mb| mb.pid == holder) {
@@ -249,17 +308,16 @@ impl Registry {
} }
} }
/// RFC 016 snapshot input: per-slot-index registry view — the actor's /// Runtime introspection input: per-slot-index registry view, giving the
/// registered names (inverted from `by_name`) and its mailbox depth (queued /// actor's registered names (inverted from `by_name`) and its mailbox
/// messages summed across every published typed channel). Built in one pass /// depth (queued messages summed across every published typed channel).
/// under the registry Leaf; the per-channel `queued_len` takes a Channel /// Carries each mailbox's full `pid` so the caller can discard a stale
/// lock, legal under the Leaf (Leaf → Channel). Carries each mailbox's full /// incarnation's entry against the slab's live generation. Names are
/// `pid` so the caller can discard a stale incarnation's entry against the /// matched to mailboxes by *full pid*, so a stale name (dead holder)
/// slab's live generation. Names are matched to mailboxes by *full pid*, so /// still annotates the corpse's own mailbox if that survives, but never a
/// a stale name (dead holder) still annotates the corpse's own mailbox if /// recycled slot's new tenant; names that attach to no mailbox are
/// that survives, but never a recycled slot's new tenant; names that attach /// dropped, since that violates no invariant and they get pruned on next
/// to no mailbox are dropped — they violate no invariant and get pruned on /// contact.
/// next contact.
pub(crate) fn introspect_map(&self) -> HashMap<u32, MailboxInfo> { pub(crate) fn introspect_map(&self) -> HashMap<u32, MailboxInfo> {
let mut names: HashMap<Pid, Vec<&'static str>> = HashMap::new(); let mut names: HashMap<Pid, Vec<&'static str>> = HashMap::new();
for (&name, &pid) in &self.by_name { for (&name, &pid) in &self.by_name {
@@ -282,8 +340,9 @@ impl Registry {
/// Single-actor form of [`introspect_map`](Self::introspect_map): the /// Single-actor form of [`introspect_map`](Self::introspect_map): the
/// registry view for one slot index, or `None` if no mailbox is published /// registry view for one slot index, or `None` if no mailbox is published
/// there. Used by `actor_info` so its cost stays proportional to the one /// there. Used by the runtime's per-actor introspection so its cost stays
/// actor rather than locking every channel in the runtime. /// proportional to the one actor rather than locking every channel in the
/// runtime.
pub(crate) fn introspect_one(&self, idx: u32) -> Option<MailboxInfo> { pub(crate) fn introspect_one(&self, idx: u32) -> Option<MailboxInfo> {
let mb = self.by_index.get(&idx)?; let mb = self.by_index.get(&idx)?;
let depth: usize = mb.channels.values().map(|c| c.sender.queued_len()).sum(); let depth: usize = mb.channels.values().map(|c| c.sender.queued_len()).sum();
@@ -301,14 +360,18 @@ fn live(inner: &crate::runtime::RuntimeInner, pid: Pid) -> bool {
inner.slot_at(pid).is_some_and(|s| s.is_live_for(pid)) inner.slot_at(pid).is_some_and(|s| s.is_live_for(pid))
} }
/// Publish the current actor's `Sender<M>` under `name`, capturing the channel /// Give the current actor's channel a name, so other actors can find and
/// so the name becomes messageable. Idempotent for the same `(name, type)`; /// message it by that name instead of needing its [`Pid`].
/// registering a *second* type under the same (or another) name on the same
/// actor just adds another channel to the actor's mailbox.
/// ///
/// Fails with [`RegisterError::NameTaken`] if the name is held by a *different* /// Calling this again with the same `(name, type)` from the same actor is
/// live actor (a binding to a dead actor is pruned and the name treated as /// harmless. Registering a *second* message type under the same (or a
/// free). Panics if called outside `Runtime::run()`. /// different) name from the same actor just adds another typed channel to
/// that actor's mailbox; it does not replace the first.
///
/// Fails with [`RegisterError::NameTaken`] if the name is currently held by a
/// *different* live actor. A name held by an actor that has since died is not
/// considered taken: it is quietly reclaimed and handed to you. Panics if
/// called outside [`run`](crate::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> {
register_with(self_pid(), name.as_str(), tx) register_with(self_pid(), name.as_str(), tx)
} }
@@ -316,8 +379,8 @@ pub fn register<M: Send + 'static>(name: Name<M>, tx: Sender<M>) -> Result<(), R
/// Bind `name` to `pid`'s mailbox and publish `tx` under `M`'s [`TypeId`], for /// 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 /// an explicit (already-live) actor rather than `self`. The shared core of
/// [`register`] (which passes `self_pid()`) and the parent-side server-name /// [`register`] (which passes `self_pid()`) and the parent-side server-name
/// bind in `gen_server` (which names a freshly spawned server before its body /// 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 /// has run, so the name resolves the instant `start()` returns. Same collision
/// rules and lock discipline as `register`. /// rules and lock discipline as `register`.
pub(crate) fn register_with<M: Send + 'static>( pub(crate) fn register_with<M: Send + 'static>(
me: Pid, me: Pid,
@@ -337,8 +400,8 @@ pub(crate) fn register_with<M: Send + 'static>(
} else { } else {
// Dead holder: free the name (and its other stale artifacts). // Dead holder: free the name (and its other stale artifacts).
// Liveness is judged against the *stored* pid, generation // Liveness is judged against the *stored* pid, generation
// included a recycled slot's live tenant no longer makes a // included, so a recycled slot's live tenant no longer makes a
// dead name read as taken (soak20 signature 2). // dead name read as taken.
reg.prune_holder(holder); reg.prune_holder(holder);
} }
} }
@@ -367,14 +430,14 @@ fn publish_channel<M: Send + 'static>(reg: &mut Registry, me: Pid, tx: Sender<M>
/// Publish the current actor's `Sender<A::Msg>` into its mailbox **without** /// Publish the current actor's `Sender<A::Msg>` into its mailbox **without**
/// binding a name, and hand back the typed [`Pid<A>`] that addresses this /// binding a name, and hand back the typed [`Pid<A>`] that addresses this
/// actor directly. This is the opt-in, lazy install of RFC 014 §5: an actor /// actor directly.
/// that wants to be reachable by a direct, identity-bound [`Pid<A>`] (rather
/// than only via a re-resolving [`Name`]) calls this once with its inbox
/// sender, then hands the returned pid out.
/// ///
/// Unlike [`register`] there is no name to collide on, and `self` is always a /// This is for an actor that wants to be reachable directly by its pid,
/// live actor inside `run()`, so this is infallible. Panics if called outside /// rather than only through a re-resolving [`Name`]: call this once with your
/// `Runtime::run()`. /// inbox sender, then hand the returned `Pid<A>` to whoever should be able to
/// message you. Unlike [`register`] there is no name to collide on, and the
/// current actor is always live while inside `run()`, so this cannot fail.
/// Panics if called outside [`run`](crate::run).
pub fn install<A: Addressable>(tx: Sender<A::Msg>) -> Pid<A> { pub fn install<A: Addressable>(tx: Sender<A::Msg>) -> Pid<A> {
let me = self_pid(); let me = self_pid();
with_runtime(|inner| { with_runtime(|inner| {
@@ -390,11 +453,12 @@ pub fn install<A: Addressable>(tx: Sender<A::Msg>) -> Pid<A> {
/// Publish `tx` into `pid`'s mailbox under `M`'s [`TypeId`], for an explicit /// Publish `tx` into `pid`'s mailbox under `M`'s [`TypeId`], for an explicit
/// (freshly minted, already-live) actor rather than `self`. The parent-side /// (freshly minted, already-live) actor rather than `self`. The parent-side
/// half of [`spawn_addr`](crate::spawn_addr): the spawner makes the inbox and /// 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 /// publishes the sender here *before* handing back the `Pid<A>`, so an
/// `send_to` on the returned pid always resolves — the address is live the /// immediate `send_to` on the returned pid always resolves. The address is
/// instant the caller holds it, with no dependence on the body having run yet. /// live the instant the caller holds it, with no dependence on the spawned
/// actor's body having run yet.
/// ///
/// Caller guarantees `pid` is the just-installed actor (Queued, this exact /// Caller guarantees `pid` is the just-installed actor (queued, this exact
/// incarnation); `publish_channel` replaces any stale leftover at the slot. /// incarnation); `publish_channel` replaces any stale leftover at the slot.
pub(crate) fn install_for<M: Send + 'static>(pid: Pid, tx: Sender<M>) { pub(crate) fn install_for<M: Send + 'static>(pid: Pid, tx: Sender<M>) {
with_runtime(|inner| { with_runtime(|inner| {
@@ -404,8 +468,9 @@ pub(crate) fn install_for<M: Send + 'static>(pid: Pid, tx: Sender<M>) {
}); });
} }
/// The single actor currently registered under `name`, or `None` if unbound or /// Look up which actor currently holds `name`, if any. Returns `None` if the
/// no longer live (the stale binding is pruned on the way out). /// name is unbound, or if it was bound to an actor that has since died (the
/// stale binding is cleared as a side effect of this call).
pub fn whereis(name: &str) -> Option<Pid> { pub fn whereis(name: &str) -> Option<Pid> {
with_runtime(|inner| { with_runtime(|inner| {
let mut reg = inner.registry.lock(); let mut reg = inner.registry.lock();
@@ -421,34 +486,37 @@ pub fn whereis(name: &str) -> Option<Pid> {
}) })
} }
/// Resolve `name` to a *typed* [`Pid<A>`] — the identity-bound counterpart of /// Like [`whereis`], but returns a *typed* [`Pid<A>`] instead of a bare
/// [`whereis`] (RFC 014 §4.4). Recovers the compile-checked /// [`Pid`], so a follow-up [`send_to`] is compile-checked instead of needing
/// [`send_to`] path from a durable name: looks the name up, /// the untyped [`send_dyn`] escape hatch. `None` if the name is unbound or its
/// then re-types the erased pid as `Pid<A>` via the unchecked /// holder has died.
/// [`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()`. /// The type `A` is not checked against what the name's holder actually
/// published: if you pick the wrong `A`, this still succeeds, but the next
/// send against the returned pid degrades to [`SendError::NoChannel`] rather
/// than reaching the wrong actor or the wrong channel.
///
/// Panics if called outside [`run`](crate::run).
pub fn lookup_as<A: Addressable>(name: &str) -> Option<Pid<A>> { pub fn lookup_as<A: Addressable>(name: &str) -> Option<Pid<A>> {
whereis(name).map(crate::pid::assert_type::<A>) whereis(name).map(crate::pid::assert_type::<A>)
} }
/// Resolve `name` to its actor's pid and a cloned `Sender<M>`, under the Leaf /// Resolve `name` to its actor's pid and a cloned `Sender<M>`, all under one
/// lock (clone-under-lock, then release). The crate-internal building block for /// lock acquisition. The crate-internal building block for `gen_server`'s
/// `gen_server`'s by-name addressing: a named server publishes its inbox as a /// by-name addressing: a named server publishes its inbox as a
/// `Sender<Envelope<G>>` (via [`register_with`]), and `whereis_server` / `call` /// `Sender<Envelope<G>>` (via [`register_with`]), and the server's `call` /
/// / `cast` recover that exact typed sender here to rebuild a `GenServerRef<G>`. /// `cast` / `whereis_server` recover that exact typed sender here to rebuild a
/// `None` if unbound, dead (pruned on the way out), or holding no `M` channel. /// `GenServerRef<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>)> { pub(crate) fn resolve_named_sender<M: Send + 'static>(name: &str) -> Option<(Pid, Sender<M>)> {
with_runtime(|inner| { with_runtime(|inner| {
let mut reg = inner.registry.lock(); let mut reg = inner.registry.lock();
let pid = *reg.by_name.get(name)?; let pid = *reg.by_name.get(name)?;
if !live(inner, pid) { if !live(inner, pid) {
// Stored-pid liveness, generation included: a name whose holder // Stored-pid liveness, generation included: a name whose holder
// died is pruned (heals) even if the slot has a new tenant // died is pruned (heals) even if the slot has a new tenant.
// previously the tenant's mailbox made the name unresolvable // Otherwise the tenant's mailbox would make the name unresolvable
// *without* pruning, wedging it for the tenant's lifetime. // without pruning, wedging it for the tenant's lifetime.
reg.prune_holder(pid); reg.prune_holder(pid);
return None; return None;
} }
@@ -459,9 +527,10 @@ pub(crate) fn resolve_named_sender<M: Send + 'static>(name: &str) -> Option<(Pid
}) })
} }
/// Remove the binding for `name`, returning the actor it pointed at if still /// Give up a name. Returns the actor it pointed at, if that actor was 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
/// it) remain. A binding to a dead actor is reported as `None`. /// bound to it) are unaffected. A binding to an already-dead actor reports
/// `None`, since there was nothing live to release.
pub fn unregister(name: &str) -> Option<Pid> { pub fn unregister(name: &str) -> Option<Pid> {
with_runtime(|inner| { with_runtime(|inner| {
let mut reg = inner.registry.lock(); let mut reg = inner.registry.lock();
@@ -470,18 +539,21 @@ pub fn unregister(name: &str) -> Option<Pid> {
}) })
} }
/// Resolve `name` to its actor's `Sender<M>` and deliver `msg`. The whole point /// Look `name` up and deliver `msg` to whichever actor currently holds it.
/// of the rework: a name you can *send* to. /// This is the point of naming an actor: a name you can send a message to
/// directly, without a separate lookup step.
/// ///
/// Errors (message returned in every case): [`SendError::Unresolved`] if no /// On failure the message comes back to you, wrapped in the [`SendError`]
/// live actor holds the name, [`SendError::NoChannel`] if that actor has no /// variant that explains why: [`SendError::Unresolved`] if no live actor
/// channel for `M`, [`SendError::Closed`] if its `M` channel's receiver is /// currently holds the name, [`SendError::NoChannel`] if the actor that holds
/// gone. Panics if called outside `Runtime::run()`. /// it never published a channel for `M`, or [`SendError::Closed`] if it
/// published one but has since dropped the receiving end. Panics if called
/// outside [`run`](crate::run).
pub fn send<M: Send + 'static>(name: Name<M>, msg: M) -> Result<(), SendError<M>> { pub fn send<M: Send + 'static>(name: Name<M>, msg: M) -> Result<(), SendError<M>> {
let key = name.as_str(); let key = name.as_str();
with_runtime(|inner| { with_runtime(|inner| {
// Resolve + clone the sender under the Leaf lock, then drop the lock // Resolve + clone the sender under the registry lock, then drop the
// before sending (a send can unpark a receiver). // lock before sending (a send can unpark a receiver).
let tx = { let tx = {
let mut reg = inner.registry.lock(); let mut reg = inner.registry.lock();
let pid = match reg.by_name.get(key) { let pid = match reg.by_name.get(key) {
@@ -489,11 +561,9 @@ pub fn send<M: Send + 'static>(name: Name<M>, msg: M) -> Result<(), SendError<M>
None => return Err(SendError::Unresolved(msg)), None => return Err(SendError::Unresolved(msg)),
}; };
if !live(inner, pid) { if !live(inner, pid) {
// Stored-pid liveness (generation included). Previously this // Stored-pid liveness (generation included), so a recycled
// checked the slot's *current* mailbox pid, so a recycled // slot's new live tenant is never mistaken for the name's
// slot's live tenant passed — and a same-typed tenant would // original (now-dead) holder.
// have received the message (misdelivery), a differently
// typed one a misleading NoChannel.
reg.prune_holder(pid); reg.prune_holder(pid);
return Err(SendError::Unresolved(msg)); return Err(SendError::Unresolved(msg));
} }
@@ -508,18 +578,19 @@ pub fn send<M: Send + 'static>(name: Name<M>, msg: M) -> Result<(), SendError<M>
/// Resolve a *raw* pid to its mailbox and deliver `msg` on the channel for `M`, /// Resolve a *raw* pid to its mailbox and deliver `msg` on the channel for `M`,
/// with **no redirect**. The stored mailbox must be this exact incarnation /// with **no redirect**. The stored mailbox must be this exact incarnation
/// (generation included) and still live; otherwise the actor this pid named is /// (generation included) and still live; otherwise the actor this pid named
/// gone and the result is [`SendError::Dead`] even when the slot now holds a /// is gone and the result is [`SendError::Dead`], even when the slot now
/// different, live actor (which we leave untouched). Shared by [`send_to`] /// holds a different, live actor (which is left untouched). Shared by
/// (typed, `M = A::Msg`, channel guaranteed on an installed actor) and /// [`send_to`] (typed, `M = A::Msg`, channel guaranteed on an installed
/// [`send_dyn`] (explicit `M`, where `NoChannel` is a real outcome). /// actor) and [`send_dyn`] (explicit `M`, where `NoChannel` is a real
/// outcome).
fn send_to_pid<M: Send + 'static>( fn send_to_pid<M: Send + 'static>(
inner: &crate::runtime::RuntimeInner, inner: &crate::runtime::RuntimeInner,
pid: Pid, pid: Pid,
msg: M, msg: M,
) -> Result<(), SendError<M>> { ) -> Result<(), SendError<M>> {
// Resolve + clone the sender under the Leaf lock, then drop the lock before // Resolve + clone the sender under the registry lock, then drop the lock
// sending (a send can unpark a receiver) — Leaf -> Channel, as name `send`. // before sending (a send can unpark a receiver), same order as `send`.
let tx = { let tx = {
let mut reg = inner.registry.lock(); let mut reg = inner.registry.lock();
match reg.by_index.get(&pid.index()).map(|m| m.pid) { match reg.by_index.get(&pid.index()).map(|m| m.pid) {
@@ -543,32 +614,36 @@ fn send_to_pid<M: Send + 'static>(
tx.send(msg).map_err(|crate::channel::SendError(m)| SendError::Closed(m)) tx.send(msg).map_err(|crate::channel::SendError(m)| SendError::Closed(m))
} }
/// Deliver `msg` to the exact actor named by `pid` — RFC 014 §4.2's direct, /// Deliver `msg` directly to the exact actor identified by `pid`. Unlike
/// identity-bound addressing mode. Unlike name-addressed [`send`] there is **no /// name-addressed [`send`], there is **no redirect**: if that specific actor
/// redirect**: if that incarnation has died the message comes back as /// has died, the message comes back as [`SendError::Dead`], even if its slot
/// [`SendError::Dead`], even if its slot now holds a different actor. /// has since been taken over by a different, live actor. Use this when you
/// already hold a `Pid<A>` and want to talk to that one actor specifically;
/// use [`send`] with a [`Name`] when you want whichever actor currently holds
/// a name.
/// ///
/// The message type is the actor's `A::Msg`, so on a live actor that has /// The message type is the actor's `A::Msg`, so on a live actor that has
/// installed its inbox (via [`install`] or [`register`]) the channel is always /// installed its inbox (via [`install`] or [`register`]) the channel is
/// present; [`SendError::NoChannel`] therefore means the actor is live but /// always present; [`SendError::NoChannel`] therefore means the actor is live
/// never published a `Pid<A>`-reachable inbox. Panics if called outside /// but never published a `Pid<A>`-reachable inbox. Panics if called outside
/// `Runtime::run()`. /// [`run`](crate::run).
pub fn send_to<A: Addressable>(pid: Pid<A>, msg: A::Msg) -> Result<(), SendError<A::Msg>> { pub fn send_to<A: Addressable>(pid: Pid<A>, msg: A::Msg) -> Result<(), SendError<A::Msg>> {
with_runtime(|inner| send_to_pid::<A::Msg>(inner, pid.erase(), msg)) with_runtime(|inner| send_to_pid::<A::Msg>(inner, pid.erase(), msg))
} }
/// The explicit bare-pid escape hatch (RFC 014 §4.6): deliver `msg` of type `M` /// The escape hatch for sending to a bare, untyped [`Pid`] when the typed
/// to `pid` when all you hold is an untyped [`Pid`] — a pid off a [`Down`], or /// [`send_to`] is unavailable, for example a pid recovered from a [`Down`]
/// out of a future `members()` — so the typed [`send_to`] is unavailable. /// notification or a group's `members()` list, where you no longer know the
/// actor's message type at compile time.
/// ///
/// This is the one send whose message type can genuinely be wrong: the actor /// Because the message type is not checked at compile time here, this is the
/// may be live yet expose no channel for `M`, returning [`SendError::NoChannel`] /// one send that can genuinely be live-but-wrong: the actor may be alive yet
/// (on the typed paths that downcast collapses to a `debug_assert`). It is /// expose no channel for `M`, in which case you get [`SendError::NoChannel`]
/// named and documented as the fallible fallback so the typed `Pid<A>` / /// back instead of a misdelivery. Liveness and redirect behavior are
/// `Name<M>` paths stay the obvious default and an agentic caller reaches for a /// otherwise identical to [`send_to`]: identity-bound, no redirect,
/// present primitive instead of inventing a workaround. Liveness is identical /// [`SendError::Dead`] once the addressed incarnation is gone. Prefer
/// to [`send_to`]: identity-bound, no redirect, [`SendError::Dead`] once the /// `send_to` with a typed `Pid<A>` whenever you have one; reach for this only
/// addressed incarnation is gone. Panics if called outside `Runtime::run()`. /// when you don't. Panics if called outside [`run`](crate::run).
/// ///
/// [`Down`]: crate::Down /// [`Down`]: crate::Down
pub fn send_dyn<M: Send + 'static>(pid: Pid, msg: M) -> Result<(), SendError<M>> { pub fn send_dyn<M: Send + 'static>(pid: Pid, msg: M) -> Result<(), SendError<M>> {