mailbox: Phase 2 — registry resolves name/pid to a messageable actor (RFC 014)
Rip out the old name<->pid bimap; rebuild registry.rs as the live mailbox directory. A name resolves to a SINGLE actor (many-per-label is pg's job); that actor owns a SET of typed channels (channels are typed, so no single mailbox), keyed by message TypeId. Resolution: name -> pid (one actor) -> Mailbox -> channel for type M. Same name + different type parameter selects different channels of the one actor, so capability separation (§4.7) needs no new type. - register<M>(Name<M>, Sender<M>): captures the current actor's channel under a name (one step, per decision). Adds channels cumulatively; NameTaken only vs a different LIVE holder; stale/dangling bindings pruned on contact. - send<M>(Name<M>, msg): the payoff — resolve, clone the Sender UNDER the Leaf lock, release, then send (Leaf -> Channel; a send can unpark). Returns the msg on Unresolved / NoChannel / Closed. - whereis -> single live pid; unregister frees only the name (mailbox/other names survive). name_of (reverse lookup) dropped — deferred to introspection. - Contained erasure: each channel is Box<dyn Any+Send> filed under TypeId::of M and downcast to its own keying type, so the downcast can't fail on good data (debug-asserted via the stored type_name). Phantom M on Name re-imposes type. - One Leaf RawMutex (the fold), so a name send stays on a single Leaf — two Leaves at once is a hard panic. runtime.rs field/init unchanged (same Registry name + new()). tests/registry.rs rewritten for the new semantics: send-by-name delivery, many typed channels on one actor, same-name/different-type routing, NameTaken, takeover across slot reuse, Unresolved/NoChannel. Also fold in a Phase 1 fixup: the phantom test key was an enum whose variants tripped dead-code under the test build (only caught now that I run -D warnings on --tests). Full suite + order-checker green, warning-free across all targets.
This commit is contained in:
+1
-1
@@ -56,7 +56,7 @@ pub use monitor::{demonitor, monitor, Down, DownReason, Monitor, MonitorId};
|
||||
pub use mutex::{LockTimeout, Mutex, MutexGuard};
|
||||
pub use pid::{Addr, Addressable, Name, Pid};
|
||||
pub use pg::{join, leave, members, pick, Incarnation, Member, NodeId};
|
||||
pub use registry::{name_of, register, unregister, whereis, RegisterError};
|
||||
pub use registry::{register, send, unregister, whereis, RegisterError, SendError};
|
||||
pub use runtime::{init, Config, Runtime};
|
||||
pub use scheduler::{
|
||||
block_on_io, request_stop, run, self_pid, sleep, spawn, spawn_under, wait_readable,
|
||||
|
||||
+1
-1
@@ -177,7 +177,7 @@ mod typed_addr_tests {
|
||||
|
||||
// A stand-in actor type with one message type, exercising `Addressable`.
|
||||
struct Counter;
|
||||
enum CounterMsg { Inc, Get }
|
||||
struct CounterMsg; // used only as a phantom key; no variants needed
|
||||
impl Addressable for Counter {
|
||||
type Msg = CounterMsg;
|
||||
}
|
||||
|
||||
+238
-99
@@ -1,32 +1,60 @@
|
||||
//! Named pid registry.
|
||||
//! Named mailbox registry — resolve a name (or pid) to a *messageable* actor.
|
||||
//!
|
||||
//! `register(name, pid)` binds a name to a live actor; `whereis(name)` looks
|
||||
//! it up; `name_of(pid)` is the inverse. Erlang semantics: at most one name
|
||||
//! per pid, at most one pid per name, registering over a live binding is an
|
||||
//! error. The registry is a *bimap* (two `HashMap`s kept in exact inverse
|
||||
//! under one lock) so both lookup directions are O(1) — `name_of` exists so
|
||||
//! supervisors, tracing, and diagnostics can label a pid without scanning.
|
||||
//! ## What changed (RFC 014)
|
||||
//!
|
||||
//! ## Cleanup is lazy
|
||||
//! 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.
|
||||
//!
|
||||
//! There is no hook in `finalize_actor` and no name field in `Slot` (which
|
||||
//! would buy into the reset-in-three-places slot invariant). Instead every
|
||||
//! operation that touches a binding checks the bound pid's liveness via the
|
||||
//! generation-checked slot word — a `Pid` is `(index, generation)` and a
|
||||
//! generation is never reused, so a stale binding is *detectable*, never
|
||||
//! misdirected. Bindings to dead actors behave as absent and are pruned on
|
||||
//! contact: `whereis`/`name_of` return `None`, `register` treats the name as
|
||||
//! free. The cost is that a dead binding lingers until something looks at it;
|
||||
//! Two facts shape the structure:
|
||||
//!
|
||||
//! 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`].
|
||||
//!
|
||||
//! Resolution is therefore: `name -> pid` (single actor) `-> Mailbox -> the
|
||||
//! channel for message type M`. A `Name<Cmd>` and a `Name<Admin>` on the *same*
|
||||
//! actor select *different* channels purely by their type parameter, so
|
||||
//! capability separation (RFC 014 §4.7) needs no extra machinery.
|
||||
//!
|
||||
//! ## Type erasure is contained
|
||||
//!
|
||||
//! Each stored channel is a `Box<dyn Any + Send>` that is concretely a
|
||||
//! `Sender<M>`, filed under `TypeId::of::<M>()`. A resolve for `M` looks up
|
||||
//! that exact `TypeId` and downcasts to `Sender<M>` — 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.
|
||||
//!
|
||||
//! ## Cleanup is lazy (prune-on-contact)
|
||||
//!
|
||||
//! 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.
|
||||
//!
|
||||
//! ## Locking
|
||||
//!
|
||||
//! One `RawMutex` (Leaf class) in `RuntimeInner`. Liveness checks under it
|
||||
//! read only the atomic slot state word — no second lock is ever taken, so
|
||||
//! the leaf rule holds trivially.
|
||||
//! 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`.
|
||||
|
||||
use crate::pid::Pid;
|
||||
use crate::scheduler::with_runtime;
|
||||
use crate::channel::Sender;
|
||||
use crate::pid::{Name, Pid};
|
||||
use crate::scheduler::{self_pid, with_runtime};
|
||||
use std::any::{type_name, Any, TypeId};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Why a [`register`] call was rejected.
|
||||
@@ -34,9 +62,8 @@ use std::collections::HashMap;
|
||||
pub enum RegisterError {
|
||||
/// The name is bound to a different, still-live actor.
|
||||
NameTaken { holder: Pid },
|
||||
/// The pid already has a (live) registered name; one name per pid.
|
||||
PidAlreadyRegistered { name: String },
|
||||
/// The pid does not refer to a live actor.
|
||||
/// The caller is not a live actor (cannot happen for `self`, kept for
|
||||
/// symmetry / future explicit-pid registration).
|
||||
NoProc,
|
||||
}
|
||||
|
||||
@@ -46,38 +73,117 @@ impl std::fmt::Display for RegisterError {
|
||||
RegisterError::NameTaken { holder } => {
|
||||
write!(f, "name is already registered to live actor {holder}")
|
||||
}
|
||||
RegisterError::PidAlreadyRegistered { name } => {
|
||||
write!(f, "pid is already registered as {name:?}")
|
||||
}
|
||||
RegisterError::NoProc => write!(f, "pid does not refer to a live actor"),
|
||||
RegisterError::NoProc => write!(f, "caller is not a live actor"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RegisterError {}
|
||||
|
||||
/// The bimap. Invariant (held under the registry lock): `by_name` and
|
||||
/// `by_pid` are exact inverses of each other at all times.
|
||||
/// Why a name-addressed [`send`] did not deliver. Carries the message back so
|
||||
/// the caller never loses it (mirrors [`crate::channel::SendError`]).
|
||||
///
|
||||
/// `Debug`/`Display` are hand-written so neither demands `M: Debug` — the
|
||||
/// payload is returned, not printed.
|
||||
pub enum SendError<M> {
|
||||
/// No live actor is currently registered under this name.
|
||||
Unresolved(M),
|
||||
/// The actor is live but exposes no channel for this message type.
|
||||
NoChannel(M),
|
||||
/// The actor's channel for this type is closed (its receiver is gone).
|
||||
Closed(M),
|
||||
}
|
||||
|
||||
impl<M> SendError<M> {
|
||||
/// Recover the undelivered message.
|
||||
pub fn into_inner(self) -> M {
|
||||
match self {
|
||||
SendError::Unresolved(m) | SendError::NoChannel(m) | SendError::Closed(m) => m,
|
||||
}
|
||||
}
|
||||
|
||||
fn variant(&self) -> &'static str {
|
||||
match self {
|
||||
SendError::Unresolved(_) => "Unresolved",
|
||||
SendError::NoChannel(_) => "NoChannel",
|
||||
SendError::Closed(_) => "Closed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> std::fmt::Debug for SendError<M> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "SendError::{}", self.variant())
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> std::fmt::Display for SendError<M> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SendError::Unresolved(_) => write!(f, "no live actor registered under that name"),
|
||||
SendError::NoChannel(_) => write!(f, "actor has no channel for this message type"),
|
||||
SendError::Closed(_) => write!(f, "the actor's channel for this type is closed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> std::error::Error for SendError<M> {}
|
||||
|
||||
/// 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
|
||||
/// observers (RFC 014 §4.5) and as the debug cross-check on the downcast.
|
||||
struct Channel {
|
||||
sender: Box<dyn Any + Send>,
|
||||
msg_type: &'static str,
|
||||
}
|
||||
|
||||
/// An actor's messageable surface: its identity plus every typed channel it has
|
||||
/// published, keyed by message [`TypeId`]. Stored once per live actor; reached
|
||||
/// by pid (directly) or by any name pointing at that pid.
|
||||
struct Mailbox {
|
||||
pid: Pid,
|
||||
channels: HashMap<TypeId, Channel>,
|
||||
}
|
||||
|
||||
impl Mailbox {
|
||||
fn new(pid: Pid) -> Self {
|
||||
Self { pid, channels: HashMap::new() }
|
||||
}
|
||||
|
||||
/// Clone the `Sender<M>` for this actor, if it has one. Called **under the
|
||||
/// registry Leaf lock**: `Sender::clone` takes a Channel lock, which is
|
||||
/// legal under a Leaf (Leaf -> Channel).
|
||||
fn clone_sender<M: Send + 'static>(&self) -> Option<Sender<M>> {
|
||||
let ch = self.channels.get(&TypeId::of::<M>())?;
|
||||
let tx = ch
|
||||
.sender
|
||||
.downcast_ref::<Sender<M>>()
|
||||
.expect("channel keyed by TypeId but downcast to its own type failed — smarm bug");
|
||||
debug_assert_eq!(ch.msg_type, type_name::<M>(), "msg_type / TypeId disagree");
|
||||
Some(tx.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// The directory. Invariant (held under the registry lock): every value in
|
||||
/// `by_name` is the index of a [`Mailbox`] present in `by_index`, and that
|
||||
/// mailbox's `pid.index()` equals the key. Stale entries (dead actors) violate
|
||||
/// nothing — they are simply pruned on contact.
|
||||
pub(crate) struct Registry {
|
||||
by_name: HashMap<String, Pid>,
|
||||
by_pid: HashMap<Pid, String>,
|
||||
/// `pid.index() -> the actor's mailbox`. The handle store.
|
||||
by_index: HashMap<u32, Mailbox>,
|
||||
/// `name -> pid.index()`. Several names may map to one actor.
|
||||
by_name: HashMap<&'static str, u32>,
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { by_name: HashMap::new(), by_pid: HashMap::new() }
|
||||
Self { by_index: HashMap::new(), by_name: HashMap::new() }
|
||||
}
|
||||
|
||||
fn remove_binding(&mut self, name: &str, pid: Pid) {
|
||||
self.by_name.remove(name);
|
||||
self.by_pid.remove(&pid);
|
||||
debug_assert_eq!(self.by_name.len(), self.by_pid.len(), "bimap out of sync");
|
||||
}
|
||||
|
||||
fn insert_binding(&mut self, name: String, pid: Pid) {
|
||||
self.by_pid.insert(pid, name.clone());
|
||||
self.by_name.insert(name, pid);
|
||||
debug_assert_eq!(self.by_name.len(), self.by_pid.len(), "bimap out of sync");
|
||||
/// Drop a dead actor's mailbox and every name that pointed at it.
|
||||
fn prune(&mut self, index: u32) {
|
||||
self.by_index.remove(&index);
|
||||
self.by_name.retain(|_, idx| *idx != index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,84 +192,117 @@ fn live(inner: &crate::runtime::RuntimeInner, pid: Pid) -> bool {
|
||||
inner.slot_at(pid).is_some_and(|s| s.is_live_for(pid))
|
||||
}
|
||||
|
||||
/// Bind `name` to the live actor `pid`.
|
||||
/// Publish the current actor's `Sender<M>` 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.
|
||||
///
|
||||
/// Fails with [`RegisterError::NoProc`] if `pid` is not live,
|
||||
/// [`RegisterError::NameTaken`] if the name is bound to a *live* actor
|
||||
/// (a binding to a dead actor is evicted and the name treated as free), and
|
||||
/// [`RegisterError::PidAlreadyRegistered`] if `pid` already has a name.
|
||||
///
|
||||
/// Panics if called outside `Runtime::run()`.
|
||||
pub fn register(name: impl Into<String>, pid: Pid) -> Result<(), RegisterError> {
|
||||
let name = name.into();
|
||||
/// 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()`.
|
||||
pub fn register<M: Send + 'static>(name: Name<M>, tx: Sender<M>) -> Result<(), RegisterError> {
|
||||
let me = self_pid();
|
||||
let key = name.as_str();
|
||||
with_runtime(|inner| {
|
||||
let mut reg = inner.registry.lock();
|
||||
if !live(inner, pid) {
|
||||
if !live(inner, me) {
|
||||
return Err(RegisterError::NoProc);
|
||||
}
|
||||
let prior = reg.by_name.get(&name).copied();
|
||||
if let Some(holder) = prior {
|
||||
if holder == pid {
|
||||
return Ok(()); // already exactly this binding: idempotent
|
||||
if let Some(&holder_idx) = reg.by_name.get(key) {
|
||||
match reg.by_index.get(&holder_idx).map(|m| m.pid) {
|
||||
Some(holder) if holder == me => {} // same actor: just add the channel below
|
||||
Some(holder) if live(inner, holder) => {
|
||||
return Err(RegisterError::NameTaken { holder });
|
||||
}
|
||||
Some(_) => reg.prune(holder_idx), // dead holder: free the name
|
||||
None => {
|
||||
reg.by_name.remove(key); // dangling name: free it
|
||||
}
|
||||
}
|
||||
if live(inner, holder) {
|
||||
return Err(RegisterError::NameTaken { holder });
|
||||
}
|
||||
// Stale binding: the holder died. Evict and treat the name as free.
|
||||
reg.remove_binding(&name, holder);
|
||||
}
|
||||
if let Some(existing) = reg.by_pid.get(&pid) {
|
||||
// `pid` is live (checked above) and pids are never reused, so this
|
||||
// binding is necessarily current — one name per pid.
|
||||
return Err(RegisterError::PidAlreadyRegistered { name: existing.clone() });
|
||||
// Insert or extend my mailbox. A leftover mailbox at this slot index
|
||||
// from a dead prior incarnation (pid mismatch) is replaced wholesale.
|
||||
let mb = reg.by_index.entry(me.index()).or_insert_with(|| Mailbox::new(me));
|
||||
if mb.pid != me {
|
||||
*mb = Mailbox::new(me);
|
||||
}
|
||||
reg.insert_binding(name, pid);
|
||||
mb.channels.insert(
|
||||
TypeId::of::<M>(),
|
||||
Channel { sender: Box::new(tx), msg_type: type_name::<M>() },
|
||||
);
|
||||
reg.by_name.insert(key, me.index());
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Look up the pid bound to `name`. `None` if unbound, or if the bound actor
|
||||
/// is no longer live (the stale binding is pruned on the way out).
|
||||
/// The single actor currently registered under `name`, or `None` if unbound or
|
||||
/// no longer live (the stale binding is pruned on the way out).
|
||||
pub fn whereis(name: &str) -> Option<Pid> {
|
||||
with_runtime(|inner| {
|
||||
let mut reg = inner.registry.lock();
|
||||
let pid = *reg.by_name.get(name)?;
|
||||
if live(inner, pid) {
|
||||
Some(pid)
|
||||
} else {
|
||||
reg.remove_binding(name, pid);
|
||||
None
|
||||
let idx = *reg.by_name.get(name)?;
|
||||
match reg.by_index.get(&idx).map(|m| m.pid) {
|
||||
Some(pid) if live(inner, pid) => Some(pid),
|
||||
Some(_) => {
|
||||
reg.prune(idx);
|
||||
None
|
||||
}
|
||||
None => {
|
||||
reg.by_name.remove(name);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The inverse lookup: the name `pid` is registered under, if any. `None` if
|
||||
/// unregistered or no longer live (pruning the stale binding).
|
||||
pub fn name_of(pid: Pid) -> Option<String> {
|
||||
with_runtime(|inner| {
|
||||
let mut reg = inner.registry.lock();
|
||||
let name = reg.by_pid.get(&pid)?.clone();
|
||||
if live(inner, pid) {
|
||||
Some(name)
|
||||
} else {
|
||||
reg.remove_binding(&name, pid);
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove the binding for `name`, returning the pid it was bound to if that
|
||||
/// actor is still live. A binding to a dead actor is pruned and reported as
|
||||
/// `None`, consistent with [`whereis`].
|
||||
/// 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`.
|
||||
pub fn unregister(name: &str) -> Option<Pid> {
|
||||
with_runtime(|inner| {
|
||||
let mut reg = inner.registry.lock();
|
||||
let pid = *reg.by_name.get(name)?;
|
||||
reg.remove_binding(name, pid);
|
||||
if live(inner, pid) {
|
||||
Some(pid)
|
||||
} else {
|
||||
None
|
||||
let idx = reg.by_name.remove(name)?;
|
||||
match reg.by_index.get(&idx).map(|m| m.pid) {
|
||||
Some(pid) if live(inner, pid) => Some(pid),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolve `name` to its actor's `Sender<M>` and deliver `msg`. The whole point
|
||||
/// of the rework: a name you can *send* to.
|
||||
///
|
||||
/// 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()`.
|
||||
pub fn send<M: Send + 'static>(name: Name<M>, msg: M) -> Result<(), SendError<M>> {
|
||||
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).
|
||||
let tx = {
|
||||
let mut reg = inner.registry.lock();
|
||||
let idx = match reg.by_name.get(key) {
|
||||
Some(&i) => i,
|
||||
None => return Err(SendError::Unresolved(msg)),
|
||||
};
|
||||
let pid = match reg.by_index.get(&idx).map(|m| m.pid) {
|
||||
Some(pid) => pid,
|
||||
None => {
|
||||
reg.by_name.remove(key);
|
||||
return Err(SendError::Unresolved(msg));
|
||||
}
|
||||
};
|
||||
if !live(inner, pid) {
|
||||
reg.prune(idx);
|
||||
return Err(SendError::Unresolved(msg));
|
||||
}
|
||||
match reg.by_index.get(&idx).and_then(Mailbox::clone_sender::<M>) {
|
||||
Some(tx) => tx,
|
||||
None => return Err(SendError::NoChannel(msg)),
|
||||
}
|
||||
};
|
||||
tx.send(msg).map_err(|crate::channel::SendError(m)| SendError::Closed(m))
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user