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.
224 lines
7.9 KiB
Rust
224 lines
7.9 KiB
Rust
//! Process identifiers.
|
|
//!
|
|
//! A `Pid` is `(index, generation)`. The index is a slot in the scheduler's
|
|
//! actor table; the generation increments every time that slot is reused.
|
|
//! A stale `Pid` (correct index, wrong generation) is a detectable error,
|
|
//! not a silent misdirection — solves the ABA problem without exhausting
|
|
//! the PID space.
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
|
pub struct Pid {
|
|
index: u32,
|
|
generation: u32,
|
|
}
|
|
|
|
impl Pid {
|
|
#[inline]
|
|
pub const fn new(index: u32, generation: u32) -> Self {
|
|
Self { index, generation }
|
|
}
|
|
|
|
#[inline]
|
|
pub const fn index(self) -> u32 { self.index }
|
|
|
|
#[inline]
|
|
pub const fn generation(self) -> u32 { self.generation }
|
|
}
|
|
|
|
impl std::fmt::Debug for Pid {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "Pid({}.{})", self.index, self.generation)
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for Pid {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "<{}.{}>", self.index, self.generation)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Typed addresses (RFC 014 — typed addressable mailboxes). Phase 1: types only.
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// Two addressing modes layered over the plain `Pid` identity, distinguished by
|
|
// their liveness semantics (RFC 014 §4.2):
|
|
//
|
|
// - `Addr<A>` — direct, identity-bound. Keyed by the *actor type* `A`; its
|
|
// message type is `A::Msg` (see [`Addressable`]). Bound to one specific actor
|
|
// incarnation: once that actor dies the send fails (generation mismatch) and
|
|
// is *not* redirected. This is the RFC's `Pid<A>`, renamed — a generic
|
|
// `Pid<A>` cannot coexist with the non-generic `Pid` identity the rest of the
|
|
// runtime is built on.
|
|
// - `Name<M>` — durable, re-resolving, location-transparent. A phantom-typed
|
|
// key over the registry, re-resolved on *every* send, so it always reaches
|
|
// whoever currently holds the name (surviving takeover; later, locating an
|
|
// actor on another node). The clustering / BEAM-interop priority mode.
|
|
//
|
|
// Both are pure value tokens: an identity (or a name) plus a zero-sized phantom
|
|
// that re-imposes the message type at the call site. The phantom is
|
|
// `fn() -> T`, so a token is unconditionally `Copy + Send + Sync` and borrows
|
|
// nothing from `T` — `T` is only ever a compile-time key. (This is also why the
|
|
// trait impls below are hand-written: a `#[derive]` would wrongly demand
|
|
// `T: Copy`/`Eq`/… on the phantom parameter.)
|
|
|
|
use std::marker::PhantomData;
|
|
|
|
/// An actor type with a single associated message type, so it can key an
|
|
/// [`Addr`]. The raw channel layer has no such trait (actors are closures over
|
|
/// channels), and `GenServer` is intrinsically multi-message and addressed via
|
|
/// its own `ServerRef`; this is the minimal hook that gives a typed,
|
|
/// identity-bound address to the actors that *do* have one message type.
|
|
/// (RFC 014 §4.2, decision (a).)
|
|
pub trait Addressable: 'static {
|
|
/// The message this actor receives. An `Addr<Self>` delivers `Self::Msg`.
|
|
type Msg: Send + 'static;
|
|
}
|
|
|
|
/// A direct, identity-bound address to a specific actor of type `A`
|
|
/// (RFC 014's `Pid<A>`): the plain [`Pid`] identity plus a phantom `A`, whose
|
|
/// message type is `A::Msg`. A send through an `Addr` targets the exact
|
|
/// incarnation the pid names; when that actor dies the send fails and is *not*
|
|
/// redirected. For a durable, re-resolving address use [`Name`].
|
|
pub struct Addr<A> {
|
|
pid: Pid,
|
|
_marker: PhantomData<fn() -> A>,
|
|
}
|
|
|
|
impl<A> Addr<A> {
|
|
/// Wrap a plain identity as a typed address. Crate-internal on purpose: a
|
|
/// public bare-`Pid` → typed-`Addr` mint is exactly the unchecked step the
|
|
/// typed API exists to avoid. The sanctioned bare-pid path is the fallible
|
|
/// `send_dyn` escape hatch (RFC 014 §4.6); typed addresses reach users via
|
|
/// spawn / resolution.
|
|
///
|
|
/// Phase 1 has no non-test constructor (this is a types-only commit); the
|
|
/// `#[cfg(test)]` comes off in Phase 2 when the handle table / spawn mints
|
|
/// these for real.
|
|
#[cfg(test)]
|
|
#[inline]
|
|
pub(crate) const fn new(pid: Pid) -> Self {
|
|
Self { pid, _marker: PhantomData }
|
|
}
|
|
|
|
/// The plain runtime identity this address points at.
|
|
#[inline]
|
|
pub const fn pid(self) -> Pid {
|
|
self.pid
|
|
}
|
|
}
|
|
|
|
impl<A> Copy for Addr<A> {}
|
|
impl<A> Clone for Addr<A> {
|
|
fn clone(&self) -> Self { *self }
|
|
}
|
|
impl<A> PartialEq for Addr<A> {
|
|
fn eq(&self, other: &Self) -> bool { self.pid == other.pid }
|
|
}
|
|
impl<A> Eq for Addr<A> {}
|
|
impl<A> std::hash::Hash for Addr<A> {
|
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { self.pid.hash(state); }
|
|
}
|
|
impl<A> std::fmt::Debug for Addr<A> {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "Addr<{}>({})", std::any::type_name::<A>(), self.pid)
|
|
}
|
|
}
|
|
|
|
/// A durable, re-resolving address: a static name plus a phantom message type
|
|
/// `M` (RFC 014's `Name<M>`). Declared as a constant and shared freely:
|
|
///
|
|
/// ```ignore
|
|
/// const COUNTER: Name<CounterMsg> = Name::new("counter");
|
|
/// ```
|
|
///
|
|
/// Unlike [`Addr`], a `Name` is resolved through the registry on *every* send,
|
|
/// so it always reaches whoever currently holds the name.
|
|
pub struct Name<M> {
|
|
name: &'static str,
|
|
_marker: PhantomData<fn() -> M>,
|
|
}
|
|
|
|
impl<M> Name<M> {
|
|
/// Bind a static string as a typed name. `const`, so names live as
|
|
/// associated constants at call sites.
|
|
#[inline]
|
|
pub const fn new(name: &'static str) -> Self {
|
|
Self { name, _marker: PhantomData }
|
|
}
|
|
|
|
/// The underlying registry key.
|
|
#[inline]
|
|
pub const fn as_str(self) -> &'static str {
|
|
self.name
|
|
}
|
|
}
|
|
|
|
impl<M> Copy for Name<M> {}
|
|
impl<M> Clone for Name<M> {
|
|
fn clone(&self) -> Self { *self }
|
|
}
|
|
impl<M> PartialEq for Name<M> {
|
|
fn eq(&self, other: &Self) -> bool { self.name == other.name }
|
|
}
|
|
impl<M> Eq for Name<M> {}
|
|
impl<M> std::hash::Hash for Name<M> {
|
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { self.name.hash(state); }
|
|
}
|
|
impl<M> std::fmt::Debug for Name<M> {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "Name<{}>({:?})", std::any::type_name::<M>(), self.name)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod typed_addr_tests {
|
|
use super::*;
|
|
|
|
// A stand-in actor type with one message type, exercising `Addressable`.
|
|
struct Counter;
|
|
struct CounterMsg; // used only as a phantom key; no variants needed
|
|
impl Addressable for Counter {
|
|
type Msg = CounterMsg;
|
|
}
|
|
|
|
fn msg_type_name<A: Addressable>() -> &'static str {
|
|
std::any::type_name::<A::Msg>()
|
|
}
|
|
|
|
#[test]
|
|
fn addr_is_a_copyable_identity_token() {
|
|
let a = Addr::<Counter>::new(Pid::new(3, 1));
|
|
let b = a; // Copy, not move
|
|
assert_eq!(a.pid(), Pid::new(3, 1));
|
|
assert_eq!(a, b);
|
|
// Same index, different generation = different actor incarnation.
|
|
assert_ne!(a, Addr::<Counter>::new(Pid::new(3, 2)));
|
|
assert!(format!("{a:?}").starts_with("Addr<"));
|
|
}
|
|
|
|
#[test]
|
|
fn name_is_a_copyable_string_token() {
|
|
const COUNTER: Name<CounterMsg> = Name::new("counter");
|
|
let n = COUNTER; // Copy
|
|
assert_eq!(n.as_str(), "counter");
|
|
assert_eq!(n, COUNTER);
|
|
assert_ne!(n, Name::<CounterMsg>::new("other"));
|
|
assert!(format!("{n:?}").contains("\"counter\""));
|
|
}
|
|
|
|
#[test]
|
|
fn addressable_exposes_the_message_type() {
|
|
assert!(msg_type_name::<Counter>().ends_with("CounterMsg"));
|
|
}
|
|
|
|
// Tokens must be usable across threads (they key cross-node addressing
|
|
// later): assert Send + Sync regardless of whether the keys are.
|
|
#[test]
|
|
fn tokens_are_send_sync_without_key_bounds() {
|
|
fn assert_send_sync<T: Send + Sync>() {}
|
|
assert_send_sync::<Addr<Counter>>();
|
|
assert_send_sync::<Name<CounterMsg>>();
|
|
}
|
|
}
|