mailbox: Phase 4a — direct identity-bound send to Pid<A> (RFC 014 §4.2)

Make a Pid<A> messageable: install + send_to, the direct addressing mode that
dies with the actor and never redirects (the counterpart to the re-resolving
Name).

- install::<A: Addressable>(tx: Sender<A::Msg>) -> Pid<A>: opt-in, lazy,
  nameless publish (RFC 014 §5). Files the actor's inbox into the by_index
  mailbox table under TypeId::of::<A::Msg>() and re-types self's identity as
  Pid<A>. Infallible: no name to collide on, self is always live in run().
- register and install now share publish_channel (the insert-or-extend-mailbox
  step factored out); register additionally binds the name. Byte-identical
  storage, so name- and pid-addressing populate the same table.
- send_to::<A>(Pid<A>, A::Msg): resolve by raw pid, deliver with NO redirect.
  The stored mailbox must be this exact incarnation (generation included) and
  live, else SendError::Dead — even when the slot now holds a different live
  actor, which is left untouched. Resolution clones the Sender under the Leaf
  lock, releases, then sends (Leaf -> Channel), as name send / pg / finalize.
- send_to_pid is the shared raw-pid core; send_dyn (§4.6) lands on it next.
- SendError gains Dead(M), the pid-path counterpart to name-path Unresolved;
  into_inner / Display / Debug updated. Name send never yields Dead, pid send
  never yields Unresolved — disjoint by construction, documented on the enum.

tests/registry.rs: install_then_send_to_pid_delivers; and the load-bearing
send_to_does_not_redirect_after_takeover (slot reused by a new incarnation, the
stale Pid<A> send returns Dead and the new occupant gets only its own message).
Full suite + order-checker green, warning-free across all targets.
This commit is contained in:
smarm-agent
2026-06-17 11:43:16 +00:00
parent f5fbb5b144
commit 3cec3ba1a1
3 changed files with 174 additions and 15 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ pub use monitor::{demonitor, monitor, Down, DownReason, Monitor, MonitorId};
pub use mutex::{LockTimeout, Mutex, MutexGuard};
pub use pid::{Addressable, Erased, Name, Pid, RawPid};
pub use pg::{join, leave, members, pick, Incarnation, Member, NodeId};
pub use registry::{register, send, unregister, whereis, RegisterError, SendError};
pub use registry::{install, register, send, send_to, 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,
+100 -8
View File
@@ -52,7 +52,7 @@
//! **Leaf -> Channel**, as `pg`/`finalize`.
use crate::channel::Sender;
use crate::pid::{Name, Pid};
use crate::pid::{Addressable, Name, Pid};
use crate::scheduler::{self_pid, with_runtime};
use std::any::{type_name, Any, TypeId};
use std::collections::HashMap;
@@ -86,8 +86,14 @@ impl std::error::Error for RegisterError {}
/// `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.
/// No live actor is currently registered under this name. Name-addressed
/// [`send`] only; the pid-addressed counterpart 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<A>` send never redirects; contrast name-addressed [`send`]). Pid
/// paths ([`send_to`] / [`send_dyn`]) only.
Dead(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).
@@ -98,13 +104,17 @@ 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,
SendError::Unresolved(m)
| SendError::Dead(m)
| SendError::NoChannel(m)
| SendError::Closed(m) => m,
}
}
fn variant(&self) -> &'static str {
match self {
SendError::Unresolved(_) => "Unresolved",
SendError::Dead(_) => "Dead",
SendError::NoChannel(_) => "NoChannel",
SendError::Closed(_) => "Closed",
}
@@ -121,6 +131,7 @@ 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::Dead(_) => write!(f, "the addressed actor is no longer the live incarnation"),
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"),
}
@@ -220,8 +231,19 @@ pub fn register<M: Send + 'static>(name: Name<M>, tx: Sender<M>) -> Result<(), R
}
}
}
// Insert or extend my mailbox. A leftover mailbox at this slot index
// from a dead prior incarnation (pid mismatch) is replaced wholesale.
// Publish (or extend) my mailbox with this channel, then bind the name.
publish_channel::<M>(&mut reg, me, tx);
reg.by_name.insert(key, me.index());
Ok(())
})
}
/// Insert or extend the current actor's mailbox with one typed channel, filed
/// under its message [`TypeId`]. Shared by [`register`] (which then binds a
/// name) and [`install`] (which does not). A leftover mailbox at this slot
/// index from a dead prior incarnation (pid mismatch) is replaced wholesale.
/// Caller holds the registry lock and has established that `me` is live.
fn publish_channel<M: Send + 'static>(reg: &mut Registry, me: Pid, tx: Sender<M>) {
let mb = reg.by_index.entry(me.index()).or_insert_with(|| Mailbox::new(me));
if mb.pid != me {
*mb = Mailbox::new(me);
@@ -230,9 +252,28 @@ pub fn register<M: Send + 'static>(name: Name<M>, tx: Sender<M>) -> Result<(), R
TypeId::of::<M>(),
Channel { sender: Box::new(tx), msg_type: type_name::<M>() },
);
reg.by_name.insert(key, me.index());
Ok(())
})
}
/// 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
/// 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<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
/// live actor inside `run()`, so this is infallible. Panics if called outside
/// `Runtime::run()`.
pub fn install<A: Addressable>(tx: Sender<A::Msg>) -> Pid<A> {
let me = self_pid();
with_runtime(|inner| {
let mut reg = inner.registry.lock();
debug_assert!(live(inner, me), "self_pid() is a live actor inside run()");
publish_channel::<A::Msg>(&mut reg, me, tx);
});
// `me` is this actor; re-type the identity as `Pid<A>` (the channel for
// `A::Msg` was just published, so the typed address is now messageable).
Pid::from_raw(me.raw())
}
/// The single actor currently registered under `name`, or `None` if unbound or
@@ -306,3 +347,54 @@ pub fn send<M: Send + 'static>(name: Name<M>, msg: M) -> Result<(), SendError<M>
tx.send(msg).map_err(|crate::channel::SendError(m)| SendError::Closed(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
/// (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).
fn send_to_pid<M: Send + 'static>(
inner: &crate::runtime::RuntimeInner,
pid: Pid,
msg: M,
) -> Result<(), SendError<M>> {
// 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`.
let tx = {
let mut reg = inner.registry.lock();
match reg.by_index.get(&pid.index()).map(|m| m.pid) {
// Exact incarnation, still alive: its `M` channel, or NoChannel.
Some(stored) if stored == pid && live(inner, pid) => {
match reg.by_index.get(&pid.index()).and_then(Mailbox::clone_sender::<M>) {
Some(tx) => tx,
None => return Err(SendError::NoChannel(msg)),
}
}
// Our incarnation's mailbox, but the actor has died: prune + Dead.
Some(stored) if stored == pid => {
reg.prune(pid.index());
return Err(SendError::Dead(msg));
}
// A different incarnation (or nothing) occupies the slot: the actor
// this pid named is gone. Do not disturb any newer occupant.
_ => return Err(SendError::Dead(msg)),
}
};
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.
///
/// 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<A>`-reachable inbox. Panics if called outside
/// `Runtime::run()`.
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))
}
+68 -1
View File
@@ -5,7 +5,10 @@
//! the root closure resolves and sends by name. A `ready` handshake closes the
//! register-then-send race without busy-waiting on `whereis`.
use smarm::{channel, register, run, send, spawn, unregister, whereis, Name, RegisterError, SendError};
use smarm::{
channel, install, register, run, send, send_to, spawn, unregister, whereis, Addressable, Name,
Pid, RegisterError, SendError,
};
const SVC: Name<u64> = Name::new("svc");
@@ -142,3 +145,67 @@ fn unregister_frees_the_name_only() {
h.join().unwrap();
});
}
// --- RFC 014 §4.2: direct, identity-bound addressing via `Pid<A>` ----------
// A stand-in single-message actor. `install::<Worker>` publishes its inbox and
// returns a `Pid<Worker>` that delivers `u64` to exactly that incarnation.
struct Worker;
impl Addressable for Worker {
type Msg = u64;
}
#[test]
fn install_then_send_to_pid_delivers() {
run(|| {
let (addr_tx, addr_rx) = channel::<Pid<Worker>>();
let h = spawn(move || {
let (tx, rx) = channel::<u64>();
let me = install::<Worker>(tx); // nameless publish, typed pid back
addr_tx.send(me).unwrap();
assert_eq!(rx.recv().unwrap(), 42);
});
let addr = addr_rx.recv().unwrap(); // worker installed, handed its Pid<Worker>
assert_eq!(addr.erase(), h.pid()); // same identity, just re-typed
send_to(addr, 42u64).unwrap();
h.join().unwrap();
});
}
#[test]
fn send_to_does_not_redirect_after_takeover() {
// The load-bearing §4.2 property: a `Pid<A>` is identity-bound. When the
// actor dies and a new incarnation reuses the slot, sending to the *old*
// address fails `Dead` — it must never silently reach the new occupant
// (that redirect is the re-resolving `Name`'s job, not a pid's).
run(|| {
let (addr_a_tx, addr_a_rx) = channel::<Pid<Worker>>();
let (rt1, rr1) = channel::<()>();
let a = spawn(move || {
let (tx, _rx) = channel::<u64>();
let me = install::<Worker>(tx);
addr_a_tx.send(me).unwrap();
rt1.send(()).unwrap(); // then return -> die
});
let a_addr = addr_a_rx.recv().unwrap();
rr1.recv().unwrap();
a.join().unwrap(); // a dead; a_addr names a dead incarnation
let (addr_b_tx, addr_b_rx) = channel::<Pid<Worker>>();
let (rt2, rr2) = channel::<()>();
let b = spawn(move || {
let (tx, rx) = channel::<u64>();
let me = install::<Worker>(tx); // reuses a's slot index, new generation
addr_b_tx.send(me).unwrap();
rt2.send(()).unwrap();
// Only b's own message ever arrives; the stale send never redirects.
assert_eq!(rx.recv().unwrap(), 7);
});
let b_addr = addr_b_rx.recv().unwrap();
rr2.recv().unwrap();
assert_ne!(b_addr.erase(), a_addr.erase()); // distinct incarnation
assert!(matches!(send_to(a_addr, 99u64), Err(SendError::Dead(_)))); // no redirect
send_to(b_addr, 7u64).unwrap(); // b's real message
b.join().unwrap();
});
}