Files
smarm/src/registry.rs
T
smarm 2c7cf0b811 feat(registry): named pid registry as a bimap
register/whereis/unregister plus the inverse lookup name_of. Erlang
semantics: one name per pid, one pid per name, registering over a live
binding errors; same-binding re-register is an idempotent Ok.

Bimap = two HashMaps held in exact inverse under one Leaf RawMutex in
RuntimeInner; the invariant is debug-asserted on every mutation. The
inverse direction costs one extra String per binding and buys O(1)
pid->name for supervisors/tracing/diagnostics.

Cleanup is lazy: no finalize_actor hook, no Slot field (which would buy
into the reset-in-three-places invariant). Liveness rides the
generation-checked slot word - pids are never reused, so a stale binding
is detectable, never misdirected - and bindings to dead actors behave as
absent, pruned on contact by whereis/name_of/register/unregister.
Liveness checks under the registry lock read only the atomic slot word,
so the leaf rule holds trivially.

send_by_name is deliberately absent: smarm has no per-pid mailbox, so
there is nothing generic to send *to* - the registry yields a Pid,
usable with monitor/link/request_stop and as routing for user channels.

Tests: roundtrip both directions, idempotence, NameTaken on live holder,
one-name-per-pid, NoProc, death-evaporates-binding (via lookup pruning
and via register's own eviction path), unregister, cross-actor lookup
wired into request_stop, and a 4-scheduler name-contention churn test.
2026-06-09 22:53:43 +00:00

170 lines
6.2 KiB
Rust

//! Named pid registry.
//!
//! `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.
//!
//! ## Cleanup is lazy
//!
//! 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;
//! 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.
use crate::pid::Pid;
use crate::scheduler::with_runtime;
use std::collections::HashMap;
/// Why a [`register`] call was rejected.
#[derive(Debug, Clone, PartialEq, Eq)]
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.
NoProc,
}
impl std::fmt::Display for RegisterError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
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"),
}
}
}
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.
pub(crate) struct Registry {
by_name: HashMap<String, Pid>,
by_pid: HashMap<Pid, String>,
}
impl Registry {
pub(crate) fn new() -> Self {
Self { by_name: HashMap::new(), by_pid: 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");
}
}
/// Is `pid` a live actor right now? Atomic slot-word read; no lock.
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`.
///
/// 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();
with_runtime(|inner| {
let mut reg = inner.registry.lock();
if !live(inner, pid) {
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 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() });
}
reg.insert_binding(name, pid);
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).
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
}
})
}
/// 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`].
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
}
})
}