diff --git a/src/registry.rs b/src/registry.rs index 363420d..5eab877 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -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 -//! delivery endpoint. This rework makes resolution yield something messageable. +//! ``` +//! use smarm::{channel, register, run, send, spawn, unregister, whereis, Name}; //! -//! Two facts shape the structure: +//! const COUNTER: Name = Name::new("counter"); //! -//! 1. **A name resolves to a single actor.** Many actors under one label is -//! what *process groups* (`pg`) are for; the registry is one-name-one-actor -//! (several names *may* point at the same actor). -//! 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`]. +//! run(|| { +//! let (ready_tx, ready_rx) = channel::<()>(); +//! let (tx, rx) = channel::(); //! -//! Resolution is therefore: `name -> pid` (single actor) `-> Mailbox -> the -//! channel for message type M`. A `Name` and a `Name` on the *same* -//! actor select *different* channels purely by their type parameter, so -//! capability separation (RFC 014 §4.7) needs no extra machinery. +//! let worker = spawn(move || { +//! // Claim the name for this actor's inbox. Any actor holding +//! // `COUNTER` can now reach this one by name. +//! 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` that is concretely a -//! `Sender`, filed under `TypeId::of::()`. A resolve for `M` looks up -//! that exact `TypeId` and downcasts to `Sender` — keyed by the very type we -//! 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. +//! // Look the name up, or just send to it directly. +//! assert_eq!(whereis("counter"), Some(worker.pid())); +//! send(COUNTER, 42).unwrap(); //! -//! ## Cleanup is lazy (prune-on-contact) +//! worker.join().unwrap(); //! -//! As before, there is no `finalize` hook and no name field on the slot. Every -//! operation that touches a binding checks the target pid's liveness via the -//! 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. +//! // The name dies with the actor: nobody holds it anymore. +//! assert_eq!(whereis("counter"), None); +//! }); +//! ``` //! -//! ## Locking +//! ## Names carry a message type //! -//! One `RawMutex` (Leaf class) in `RuntimeInner`, exactly like the old -//! registry. The fold (name index *and* handles under the one lock) is what -//! keeps a name-addressed `send` on a single Leaf — `raw_mutex` panics on a -//! second Leaf acquired while one is held. The send path clones the `Sender` -//! **under** the Leaf lock (a `Sender::clone` takes a Channel lock, permitted -//! under a Leaf), then **releases** the Leaf and only *then* sends — a send can -//! unpark a receiver, and wakeup-bearing work runs outside the Leaf. Order is -//! **Leaf -> Channel**, as `pg`/`finalize`. +//! A [`Name`] is a plain string plus a type parameter `M`: the message +//! type that name expects to receive. [`Name::new`] is `const`, so the usual +//! pattern is a module-level constant like `COUNTER` above, shared by every +//! caller. The type parameter means a name is only ever sent the kind of +//! message it was declared for. If two different constants share the same +//! string but have different message types, they still address two +//! independent channels on the same actor: registering both just gives that +//! 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` that +//! is concretely a `Sender`; 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::pid::{Addressable, Name, Pid}; @@ -80,28 +133,33 @@ impl std::fmt::Display for RegisterError { impl std::error::Error for RegisterError {} -/// Why a name-addressed [`send`] did not deliver. Carries the message back so -/// the caller never loses it (mirrors [`crate::channel::SendError`]). +/// Why a send did not deliver. Every variant carries the undelivered message +/// 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 -/// payload is returned, not printed. +/// `Debug` and `Display` are hand-written so neither requires `M: Debug`, +/// since the payload is handed back to you, not printed. pub enum SendError { - /// No live actor is currently registered under this name. Name-addressed - /// [`send`] only; the pid-addressed counterpart is [`SendError::Dead`]. + /// No live actor is currently registered under this name. Returned only + /// by name-addressed [`send`]; the pid-addressed counterpart of "nothing + /// there" is [`SendError::Dead`]. Unresolved(M), - /// The pid-addressed actor is no longer the live incarnation this pid names - /// — it has died, even if its slot now holds a *different* actor (a direct - /// `Pid` send never redirects; contrast name-addressed [`send`]). Pid - /// paths ([`send_to`] / [`send_dyn`]) only. + /// The actor this pid identifies has died, even if its slot has since + /// been taken over by a different, live actor. A direct `Pid` send + /// never redirects to that new occupant; contrast name-addressed + /// [`send`], which would reach it. Returned by the pid-addressed sends, + /// [`send_to`] and [`send_dyn`]. 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), - /// 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), - /// 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. + /// No live member was available to deliver to: returned by + /// [`dispatch`](crate::dispatch) when the target process group is empty + /// or every member in it has died. The name-addressed counterpart of + /// this case is [`SendError::Unresolved`]. NoMember(M), } @@ -150,9 +208,9 @@ impl std::error::Error for SendError {} /// A registry-stored channel, type-erased over its message type. The stored /// object must serve two readers: `clone_sender` (downcast back to the concrete -/// `Sender`) and the RFC 016 snapshot (queued length without knowing `M`). -/// A bare `Box` gives the first but not the second, so we erase behind -/// this small trait instead. +/// `Sender`) and the runtime introspection snapshot (queued length without +/// knowing `M`). A bare `Box` gives the first but not the second, so +/// we erase behind this small trait instead. trait ErasedSender: Send { fn as_any(&self) -> &dyn Any; fn queued_len(&self) -> usize; @@ -169,7 +227,7 @@ impl ErasedSender for Sender { /// One typed channel of an actor, type-erased. Concretely a `Sender` filed /// under `TypeId::of::()`; `msg_type` is `type_name::()`, 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 { sender: Box, 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 /// incarnation can be filtered against the slab. Covers only *published* /// 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 /// `by_name` is the full [`Pid`] (index *and* generation) of an actor that /// published a [`Mailbox`] into `by_index` at registration time. Stale entries -/// (dead holders — including holders whose slot has since been re-tenanted by -/// a different actor) violate nothing — they are pruned on contact, and the +/// (dead holders, including holders whose slot has since been re-tenanted by +/// a different actor) violate nothing: they are pruned on contact, and the /// generation makes "dead" decidable even after slot reuse. pub(crate) struct Registry { /// `pid.index() -> the actor's mailbox`. The handle store. by_index: HashMap, /// `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 - /// holder from the live actor now tenanting its recycled slot, which made - /// such a name read as live-held — unresolvable *and* unregisterable — and - /// would misdeliver to a same-typed tenant (soak20 signature 2). + /// holder from the live actor now tenanting its recycled slot. Comparing + /// only the index would make such a name read as live-held (unresolvable + /// and unregisterable at once) and could misdeliver to whatever new, + /// same-typed actor now sits in that slot. by_name: HashMap<&'static str, Pid>, } @@ -238,10 +297,10 @@ impl Registry { Self { by_index: HashMap::new(), by_name: HashMap::new() } } - /// Drop a dead holder's artifacts: every name bound to it, and its mailbox - /// — but only while the mailbox is still *its own*. A recycled slot's - /// mailbox belongs to the live tenant (publish replaces it wholesale on - /// pid mismatch) and is left untouched. + /// Drop a dead holder's artifacts: every name bound to it, and its + /// mailbox, but only while the mailbox is still *its own*. A recycled + /// slot's mailbox belongs to the live tenant (publish replaces it + /// wholesale on pid mismatch) and is left untouched. fn prune_holder(&mut self, holder: Pid) { self.by_name.retain(|_, p| *p != 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 - /// registered names (inverted from `by_name`) and its mailbox depth (queued - /// messages summed across every published typed channel). Built in one pass - /// under the registry Leaf; the per-channel `queued_len` takes a Channel - /// lock, legal under the Leaf (Leaf → Channel). Carries each mailbox's full - /// `pid` so the caller can discard a stale incarnation's entry against the - /// slab's live generation. Names are matched to mailboxes by *full pid*, so - /// a stale name (dead holder) still annotates the corpse's own mailbox if - /// that survives, but never a recycled slot's new tenant; names that attach - /// to no mailbox are dropped — they violate no invariant and get pruned on - /// next contact. + /// Runtime introspection input: per-slot-index registry view, giving the + /// actor's registered names (inverted from `by_name`) and its mailbox + /// depth (queued messages summed across every published typed channel). + /// Carries each mailbox's full `pid` so the caller can discard a stale + /// incarnation's entry against the slab's live generation. Names are + /// matched to mailboxes by *full pid*, so a stale name (dead holder) + /// still annotates the corpse's own mailbox if that survives, but never a + /// recycled slot's new tenant; names that attach to no mailbox are + /// dropped, since that violates no invariant and they get pruned on next + /// contact. pub(crate) fn introspect_map(&self) -> HashMap { let mut names: HashMap> = HashMap::new(); for (&name, &pid) in &self.by_name { @@ -282,8 +340,9 @@ impl Registry { /// Single-actor form of [`introspect_map`](Self::introspect_map): the /// 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 - /// actor rather than locking every channel in the runtime. + /// there. Used by the runtime's per-actor introspection so its cost stays + /// proportional to the one actor rather than locking every channel in the + /// runtime. pub(crate) fn introspect_one(&self, idx: u32) -> Option { let mb = self.by_index.get(&idx)?; 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)) } -/// Publish the current actor's `Sender` under `name`, capturing the channel -/// so the name becomes messageable. Idempotent for the same `(name, type)`; -/// registering a *second* type under the same (or another) name on the same -/// actor just adds another channel to the actor's mailbox. +/// Give the current actor's channel a name, so other actors can find and +/// message it by that name instead of needing its [`Pid`]. /// -/// Fails with [`RegisterError::NameTaken`] if the name is held by a *different* -/// live actor (a binding to a dead actor is pruned and the name treated as -/// free). Panics if called outside `Runtime::run()`. +/// Calling this again with the same `(name, type)` from the same actor is +/// harmless. Registering a *second* message type under the same (or a +/// 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(name: Name, tx: Sender) -> Result<(), RegisterError> { register_with(self_pid(), name.as_str(), tx) } @@ -316,8 +379,8 @@ pub fn register(name: Name, tx: Sender) -> Result<(), R /// 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 +/// 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( me: Pid, @@ -337,8 +400,8 @@ pub(crate) fn register_with( } else { // Dead holder: free the name (and its other stale artifacts). // Liveness is judged against the *stored* pid, generation - // included — a recycled slot's live tenant no longer makes a - // dead name read as taken (soak20 signature 2). + // included, so a recycled slot's live tenant no longer makes a + // dead name read as taken. reg.prune_holder(holder); } } @@ -367,14 +430,14 @@ fn publish_channel(reg: &mut Registry, me: Pid, tx: Sender /// Publish the current actor's `Sender` into its mailbox **without** /// binding a name, and hand back the typed [`Pid`] that addresses this -/// actor directly. This is the opt-in, lazy install of RFC 014 §5: an actor -/// that wants to be reachable by a direct, identity-bound [`Pid`] (rather -/// than only via a re-resolving [`Name`]) calls this once with its inbox -/// sender, then hands the returned pid out. +/// actor directly. /// -/// Unlike [`register`] there is no name to collide on, and `self` is always a -/// live actor inside `run()`, so this is infallible. Panics if called outside -/// `Runtime::run()`. +/// This is for an actor that wants to be reachable directly by its pid, +/// rather than only through a re-resolving [`Name`]: call this once with your +/// inbox sender, then hand the returned `Pid` 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(tx: Sender) -> Pid { let me = self_pid(); with_runtime(|inner| { @@ -390,11 +453,12 @@ pub fn install(tx: Sender) -> Pid { /// 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`, 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. +/// publishes the sender here *before* handing back the `Pid`, 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 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. pub(crate) fn install_for(pid: Pid, tx: Sender) { with_runtime(|inner| { @@ -404,8 +468,9 @@ pub(crate) fn install_for(pid: Pid, tx: Sender) { }); } -/// The single actor currently registered under `name`, or `None` if unbound or -/// no longer live (the stale binding is pruned on the way out). +/// Look up which actor currently holds `name`, if any. Returns `None` if the +/// 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 { with_runtime(|inner| { let mut reg = inner.registry.lock(); @@ -421,34 +486,37 @@ pub fn whereis(name: &str) -> Option { }) } -/// Resolve `name` to a *typed* [`Pid`] — the identity-bound counterpart of -/// [`whereis`] (RFC 014 §4.4). Recovers the compile-checked -/// [`send_to`] path from a durable name: looks the name up, -/// then re-types the erased pid as `Pid` 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. +/// Like [`whereis`], but returns a *typed* [`Pid`] instead of a bare +/// [`Pid`], so a follow-up [`send_to`] is compile-checked instead of needing +/// the untyped [`send_dyn`] escape hatch. `None` if the name is unbound or its +/// holder has died. /// -/// 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(name: &str) -> Option> { whereis(name).map(crate::pid::assert_type::) } -/// Resolve `name` to its actor's pid and a cloned `Sender`, 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>` (via [`register_with`]), and `whereis_server` / `call` -/// / `cast` recover that exact typed sender here to rebuild a `GenServerRef`. -/// `None` if unbound, dead (pruned on the way out), or holding no `M` channel. +/// Resolve `name` to its actor's pid and a cloned `Sender`, all under one +/// lock acquisition. The crate-internal building block for `gen_server`'s +/// by-name addressing: a named server publishes its inbox as a +/// `Sender>` (via [`register_with`]), and the server's `call` / +/// `cast` / `whereis_server` recover that exact typed sender here to rebuild a +/// `GenServerRef`. `None` if unbound, dead (pruned on the way out), or +/// holding no `M` channel. pub(crate) fn resolve_named_sender(name: &str) -> Option<(Pid, Sender)> { with_runtime(|inner| { let mut reg = inner.registry.lock(); let pid = *reg.by_name.get(name)?; if !live(inner, pid) { // Stored-pid liveness, generation included: a name whose holder - // died is pruned (heals) even if the slot has a new tenant — - // previously the tenant's mailbox made the name unresolvable - // *without* pruning, wedging it for the tenant's lifetime. + // died is pruned (heals) even if the slot has a new tenant. + // Otherwise the tenant's mailbox would make the name unresolvable + // without pruning, wedging it for the tenant's lifetime. reg.prune_holder(pid); return None; } @@ -459,9 +527,10 @@ pub(crate) fn resolve_named_sender(name: &str) -> Option<(Pid }) } -/// 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 -/// it) remain. A binding to a dead actor is reported as `None`. +/// 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 +/// 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 { with_runtime(|inner| { let mut reg = inner.registry.lock(); @@ -470,18 +539,21 @@ pub fn unregister(name: &str) -> Option { }) } -/// Resolve `name` to its actor's `Sender` and deliver `msg`. The whole point -/// of the rework: a name you can *send* to. +/// Look `name` up and deliver `msg` to whichever actor currently holds it. +/// 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 -/// live actor holds the name, [`SendError::NoChannel`] if that actor has no -/// channel for `M`, [`SendError::Closed`] if its `M` channel's receiver is -/// gone. Panics if called outside `Runtime::run()`. +/// On failure the message comes back to you, wrapped in the [`SendError`] +/// variant that explains why: [`SendError::Unresolved`] if no live actor +/// currently holds the name, [`SendError::NoChannel`] if the actor that holds +/// 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(name: Name, msg: M) -> Result<(), SendError> { let key = name.as_str(); with_runtime(|inner| { - // Resolve + clone the sender under the Leaf lock, then drop the lock - // before sending (a send can unpark a receiver). + // Resolve + clone the sender under the registry lock, then drop the + // lock before sending (a send can unpark a receiver). let tx = { let mut reg = inner.registry.lock(); let pid = match reg.by_name.get(key) { @@ -489,11 +561,9 @@ pub fn send(name: Name, msg: M) -> Result<(), SendError None => return Err(SendError::Unresolved(msg)), }; if !live(inner, pid) { - // Stored-pid liveness (generation included). Previously this - // checked the slot's *current* mailbox pid, so a recycled - // slot's live tenant passed — and a same-typed tenant would - // have received the message (misdelivery), a differently - // typed one a misleading NoChannel. + // Stored-pid liveness (generation included), so a recycled + // slot's new live tenant is never mistaken for the name's + // original (now-dead) holder. reg.prune_holder(pid); return Err(SendError::Unresolved(msg)); } @@ -508,18 +578,19 @@ pub fn send(name: Name, msg: M) -> Result<(), SendError /// 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 -/// (generation included) and still live; otherwise the actor this pid named is -/// gone and the result is [`SendError::Dead`] — even when the slot now holds a -/// different, live actor (which we leave untouched). Shared by [`send_to`] -/// (typed, `M = A::Msg`, channel guaranteed on an installed actor) and -/// [`send_dyn`] (explicit `M`, where `NoChannel` is a real outcome). +/// (generation included) and still live; otherwise the actor this pid named +/// is gone and the result is [`SendError::Dead`], even when the slot now +/// holds a different, live actor (which is left untouched). Shared by +/// [`send_to`] (typed, `M = A::Msg`, channel guaranteed on an installed +/// actor) and [`send_dyn`] (explicit `M`, where `NoChannel` is a real +/// outcome). fn send_to_pid( inner: &crate::runtime::RuntimeInner, pid: Pid, msg: M, ) -> Result<(), SendError> { - // Resolve + clone the sender under the Leaf lock, then drop the lock before - // sending (a send can unpark a receiver) — Leaf -> Channel, as name `send`. + // Resolve + clone the sender under the registry lock, then drop the lock + // before sending (a send can unpark a receiver), same order as `send`. let tx = { let mut reg = inner.registry.lock(); match reg.by_index.get(&pid.index()).map(|m| m.pid) { @@ -543,32 +614,36 @@ fn send_to_pid( 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, -/// identity-bound addressing mode. Unlike name-addressed [`send`] there is **no -/// redirect**: if that incarnation has died the message comes back as -/// [`SendError::Dead`], even if its slot now holds a different actor. +/// Deliver `msg` directly to the exact actor identified by `pid`. Unlike +/// name-addressed [`send`], there is **no redirect**: if that specific actor +/// has died, the message comes back as [`SendError::Dead`], even if its slot +/// has since been taken over by a different, live actor. Use this when you +/// already hold a `Pid` 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 -/// installed its inbox (via [`install`] or [`register`]) the channel is always -/// present; [`SendError::NoChannel`] therefore means the actor is live but -/// never published a `Pid`-reachable inbox. Panics if called outside -/// `Runtime::run()`. +/// installed its inbox (via [`install`] or [`register`]) the channel is +/// always present; [`SendError::NoChannel`] therefore means the actor is live +/// but never published a `Pid`-reachable inbox. Panics if called outside +/// [`run`](crate::run). pub fn send_to(pid: Pid, msg: A::Msg) -> Result<(), SendError> { with_runtime(|inner| send_to_pid::(inner, pid.erase(), msg)) } -/// The explicit bare-pid escape hatch (RFC 014 §4.6): deliver `msg` of type `M` -/// to `pid` when all you hold is an untyped [`Pid`] — a pid off a [`Down`], or -/// out of a future `members()` — so the typed [`send_to`] is unavailable. +/// The escape hatch for sending to a bare, untyped [`Pid`] when the typed +/// [`send_to`] is unavailable, for example a pid recovered from a [`Down`] +/// 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 -/// may be live yet expose no channel for `M`, returning [`SendError::NoChannel`] -/// (on the typed paths that downcast collapses to a `debug_assert`). It is -/// named and documented as the fallible fallback so the typed `Pid` / -/// `Name` paths stay the obvious default and an agentic caller reaches for a -/// present primitive instead of inventing a workaround. Liveness is identical -/// to [`send_to`]: identity-bound, no redirect, [`SendError::Dead`] once the -/// addressed incarnation is gone. Panics if called outside `Runtime::run()`. +/// Because the message type is not checked at compile time here, this is the +/// one send that can genuinely be live-but-wrong: the actor may be alive yet +/// expose no channel for `M`, in which case you get [`SendError::NoChannel`] +/// back instead of a misdelivery. Liveness and redirect behavior are +/// otherwise identical to [`send_to`]: identity-bound, no redirect, +/// [`SendError::Dead`] once the addressed incarnation is gone. Prefer +/// `send_to` with a typed `Pid` whenever you have one; reach for this only +/// when you don't. Panics if called outside [`run`](crate::run). /// /// [`Down`]: crate::Down pub fn send_dyn(pid: Pid, msg: M) -> Result<(), SendError> {