mailbox: Phase 3 — typed Pid<A> over a raw inner identity (RFC 014)
Fold the made-up Addr<A> back into the pid, where it belonged. The typed
handle IS the pid now.
- RawPid { index, generation }: today's Pid, renamed — the raw numbers, the key
for identity-only plumbing and the heterogeneous monitor/link/pg tables.
- Pid<A = Erased>: RawPid + phantom actor type. Both an identity and a direct,
identity-bound address; when A: Addressable a send delivers A::Msg to exactly
this incarnation (next phase). Hand-written impls (eq/hash/fmt on the raw
only, no A bounds); fn()->A phantom keeps it Copy+Send+Sync for any A.
- Erased: uninhabited, deliberately NOT Addressable, so a typed send to a
Pid<Erased> won't compile — that's what send_dyn (section 4.6) will be for. It
is the default type arg, so every plain "Pid" written today still compiles as
Pid<Erased>; that, plus the fact that nothing destructures pid fields, is why
an identity change touching 18 files cascaded to zero internal edits.
- Addr<A> deleted; Name<M> / the Phase-2 registry unchanged (they key on raw).
Per the call to make the consumers take typed pids: the user-facing identity
APIs — monitor, link, unlink, request_stop, spawn_under(supervisor), pg::join,
pg::leave — are now generic over A and erase at the boundary; their bodies are
byte-for-byte unchanged. Pure plumbing (unpark, set_current_pid,
register_supervisor_channel) stays erased — it only ever sees internal identity.
Phase 1's Addr tests become Pid<A> tests (identity/copy/erase/Debug, Send+Sync).
Full suite + order-checker green, warning-free across all targets.
This commit is contained in:
+1
-1
@@ -54,7 +54,7 @@ pub use gen_server::{CallError, CallTimeoutError, CastError, GenServer, ServerBu
|
||||
pub use link::{link, trap_exit, unlink, ExitSignal};
|
||||
pub use monitor::{demonitor, monitor, Down, DownReason, Monitor, MonitorId};
|
||||
pub use mutex::{LockTimeout, Mutex, MutexGuard};
|
||||
pub use pid::{Addr, Addressable, Name, Pid};
|
||||
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 runtime::{init, Config, Runtime};
|
||||
|
||||
+4
-2
@@ -95,7 +95,8 @@ pub fn trap_exit() -> Receiver<ExitSignal> {
|
||||
/// this delivers an immediate [`DownReason::NoProc`] exit signal to the caller
|
||||
/// (a message if trapping, otherwise a cooperative stop). Linking yourself, or
|
||||
/// re-linking an existing peer, is a no-op.
|
||||
pub fn link(target: Pid) {
|
||||
pub fn link<A>(target: Pid<A>) {
|
||||
let target = target.erase();
|
||||
let me = self_pid();
|
||||
if target == me {
|
||||
return;
|
||||
@@ -163,7 +164,8 @@ pub fn link(target: Pid) {
|
||||
///
|
||||
/// After this, neither actor's death propagates to the other. A no-op if the
|
||||
/// two were not linked.
|
||||
pub fn unlink(target: Pid) {
|
||||
pub fn unlink<A>(target: Pid<A>) {
|
||||
let target = target.erase();
|
||||
let me = self_pid();
|
||||
if target == me {
|
||||
return;
|
||||
|
||||
+2
-1
@@ -106,7 +106,8 @@ pub struct Monitor {
|
||||
/// If `target` is still live, the `Down` arrives when it terminates. If
|
||||
/// `target` is already gone, a [`DownReason::NoProc`] `Down` is queued
|
||||
/// immediately so the caller's `rx.recv()` returns without parking.
|
||||
pub fn monitor(target: Pid) -> Monitor {
|
||||
pub fn monitor<A>(target: Pid<A>) -> Monitor {
|
||||
let target = target.erase();
|
||||
let (tx, rx) = channel::<Down>();
|
||||
|
||||
// Register under the target's cold lock. `tx.clone()` takes the channel's
|
||||
|
||||
@@ -320,8 +320,9 @@ fn live(inner: &crate::runtime::RuntimeInner, pid: Pid) -> bool {
|
||||
/// down.
|
||||
///
|
||||
/// Panics if called outside `Runtime::run()`.
|
||||
pub fn join(group: impl Into<String>, pid: Pid) -> bool {
|
||||
pub fn join<A>(group: impl Into<String>, pid: Pid<A>) -> bool {
|
||||
let group = group.into();
|
||||
let pid = pid.erase();
|
||||
// Install the monitor BEFORE taking the group lock: monitor() acquires the
|
||||
// target's cold lock (Leaf), and two Leaf locks are never held at once.
|
||||
let mon = monitor(pid);
|
||||
@@ -350,7 +351,8 @@ pub fn join(group: impl Into<String>, pid: Pid) -> bool {
|
||||
/// removed. The membership's monitor is demonitored and dropped.
|
||||
///
|
||||
/// Panics if called outside `Runtime::run()`.
|
||||
pub fn leave(group: &str, pid: Pid) -> bool {
|
||||
pub fn leave<A>(group: &str, pid: Pid<A>) -> bool {
|
||||
let pid = pid.erase();
|
||||
let (removed, reaped) = with_runtime(|inner| {
|
||||
let member = member_for(inner, pid);
|
||||
let mut pg = inner.process_groups.lock();
|
||||
|
||||
+148
-102
@@ -1,129 +1,161 @@
|
||||
//! 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.
|
||||
//! Identity is `(index, generation)`: the index is a slot in the scheduler's
|
||||
//! actor table, the generation increments every time that slot is reused, so a
|
||||
//! stale id (right index, wrong generation) is a *detectable* error rather than
|
||||
//! a silent misdirection — the ABA problem solved without exhausting the id
|
||||
//! space. Those raw numbers live in [`RawPid`].
|
||||
//!
|
||||
//! The public identity is the *typed* [`Pid<A>`] (RFC 014): `RawPid` plus a
|
||||
//! phantom actor type, so a pid is simultaneously an identity and a direct,
|
||||
//! identity-bound address. `Pid<Erased>` — the default — is the untyped pid
|
||||
//! used for identity-only plumbing and for actors with no single message type
|
||||
//! (raw `spawn`, gen_servers). Resolving a name yields the durable, re-resolving
|
||||
//! [`Name`] instead.
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// The raw identity numbers, with no actor type. The key for everything that
|
||||
/// only cares about *which* actor: slab indexing, generation checks, and the
|
||||
/// heterogeneous monitor / link / pg tables (which hold actors of every type at
|
||||
/// once, so they cannot be parameterised by one).
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Pid {
|
||||
pub struct RawPid {
|
||||
index: u32,
|
||||
generation: u32,
|
||||
}
|
||||
|
||||
impl Pid {
|
||||
impl RawPid {
|
||||
#[inline]
|
||||
pub const fn new(index: u32, generation: u32) -> Self {
|
||||
Self { index, generation }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn index(self) -> u32 { self.index }
|
||||
|
||||
pub const fn index(self) -> u32 {
|
||||
self.index
|
||||
}
|
||||
#[inline]
|
||||
pub const fn generation(self) -> u32 { self.generation }
|
||||
pub const fn generation(self) -> u32 {
|
||||
self.generation
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Pid {
|
||||
impl std::fmt::Debug for RawPid {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Pid({}.{})", self.index, self.generation)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Pid {
|
||||
impl std::fmt::Display for RawPid {
|
||||
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.)
|
||||
/// Phantom actor type for a pid that has no single message type: raw `spawn`
|
||||
/// actors, gen_servers (intrinsically multi-message, addressed via `ServerRef`),
|
||||
/// and every identity-only context. Deliberately **not** [`Addressable`], so a
|
||||
/// typed `send` to a `Pid<Erased>` does not compile; the runtime-checked
|
||||
/// `send_dyn` escape hatch (RFC 014 §4.6) is the sanctioned bare-pid path.
|
||||
pub enum Erased {}
|
||||
|
||||
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,
|
||||
/// A process identifier parameterised by the actor's type `A` (default
|
||||
/// [`Erased`]). Wraps the raw `(index, generation)` plus a zero-sized phantom,
|
||||
/// so a `Pid<A>` is both an identity and a direct, identity-bound address: when
|
||||
/// `A: Addressable`, a `send` delivers `A::Msg` to exactly the incarnation this
|
||||
/// pid names — no redirect (contrast the re-resolving [`Name`]).
|
||||
///
|
||||
/// Equality, hashing, and formatting are the raw identity's; the phantom is
|
||||
/// `fn() -> A`, so `Pid<A>` is unconditionally `Copy + Send + Sync` and borrows
|
||||
/// nothing from `A`. The trait impls are hand-written so no `A: Trait` bound
|
||||
/// leaks in from a `#[derive]`.
|
||||
pub struct Pid<A = Erased> {
|
||||
raw: RawPid,
|
||||
_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)]
|
||||
impl Pid<Erased> {
|
||||
/// Build an untyped pid from raw numbers. The runtime mints identities
|
||||
/// here; typing happens at typed-actor boundaries via [`Pid::from_raw`].
|
||||
#[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
|
||||
pub const fn new(index: u32, generation: u32) -> Self {
|
||||
Self { raw: RawPid::new(index, generation), _marker: PhantomData }
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> Copy for Addr<A> {}
|
||||
impl<A> Clone for Addr<A> {
|
||||
fn clone(&self) -> Self { *self }
|
||||
impl<A> Pid<A> {
|
||||
/// Wrap a raw identity as a typed pid. Crate-internal: minting a typed pid
|
||||
/// from raw numbers asserts an actor's type *unchecked*, which is exactly
|
||||
/// what the typed API exists to avoid outside the runtime's own spawn /
|
||||
/// resolution paths.
|
||||
#[inline]
|
||||
pub(crate) const fn from_raw(raw: RawPid) -> Self {
|
||||
Self { raw, _marker: PhantomData }
|
||||
}
|
||||
|
||||
/// The raw identity, dropping the actor type — the key for identity-only
|
||||
/// tables and internal plumbing.
|
||||
#[inline]
|
||||
pub const fn raw(self) -> RawPid {
|
||||
self.raw
|
||||
}
|
||||
|
||||
/// Forget the actor type.
|
||||
#[inline]
|
||||
pub const fn erase(self) -> Pid<Erased> {
|
||||
Pid::from_raw(self.raw)
|
||||
}
|
||||
|
||||
/// Slot index in the actor table.
|
||||
#[inline]
|
||||
pub const fn index(self) -> u32 {
|
||||
self.raw.index()
|
||||
}
|
||||
|
||||
/// Reuse generation of the slot (ABA guard).
|
||||
#[inline]
|
||||
pub const fn generation(self) -> u32 {
|
||||
self.raw.generation()
|
||||
}
|
||||
}
|
||||
impl<A> PartialEq for Addr<A> {
|
||||
fn eq(&self, other: &Self) -> bool { self.pid == other.pid }
|
||||
|
||||
impl<A> Copy for Pid<A> {}
|
||||
impl<A> Clone for Pid<A> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
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> PartialEq for Pid<A> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.raw == other.raw
|
||||
}
|
||||
}
|
||||
impl<A> std::fmt::Debug for Addr<A> {
|
||||
impl<A> Eq for Pid<A> {}
|
||||
impl<A> std::hash::Hash for Pid<A> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.raw.hash(state);
|
||||
}
|
||||
}
|
||||
impl<A> std::fmt::Debug for Pid<A> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Addr<{}>({})", std::any::type_name::<A>(), self.pid)
|
||||
std::fmt::Debug::fmt(&self.raw, f)
|
||||
}
|
||||
}
|
||||
impl<A> std::fmt::Display for Pid<A> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.raw, f)
|
||||
}
|
||||
}
|
||||
|
||||
/// An actor type with a single associated message type, so a [`Pid<Self>`] is a
|
||||
/// typed address. The raw channel layer has no such trait (actors are closures
|
||||
/// over channels) and `GenServer` is intrinsically multi-message (addressed via
|
||||
/// its own `ServerRef`); this is the minimal hook that lets the single-message
|
||||
/// actors carry their message type in their pid. (RFC 014 §4.2.)
|
||||
pub trait Addressable: 'static {
|
||||
/// The message this actor receives. A `Pid<Self>` delivers `Self::Msg`.
|
||||
type Msg: Send + 'static;
|
||||
}
|
||||
|
||||
/// 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:
|
||||
@@ -132,7 +164,7 @@ impl<A> std::fmt::Debug for Addr<A> {
|
||||
/// const COUNTER: Name<CounterMsg> = Name::new("counter");
|
||||
/// ```
|
||||
///
|
||||
/// Unlike [`Addr`], a `Name` is resolved through the registry on *every* send,
|
||||
/// Unlike a [`Pid`], 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,
|
||||
@@ -156,14 +188,20 @@ impl<M> Name<M> {
|
||||
|
||||
impl<M> Copy for Name<M> {}
|
||||
impl<M> Clone for Name<M> {
|
||||
fn clone(&self) -> Self { *self }
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
impl<M> PartialEq for Name<M> {
|
||||
fn eq(&self, other: &Self) -> bool { self.name == other.name }
|
||||
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); }
|
||||
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 {
|
||||
@@ -172,7 +210,7 @@ impl<M> std::fmt::Debug for Name<M> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod typed_addr_tests {
|
||||
mod typed_pid_tests {
|
||||
use super::*;
|
||||
|
||||
// A stand-in actor type with one message type, exercising `Addressable`.
|
||||
@@ -187,14 +225,22 @@ mod typed_addr_tests {
|
||||
}
|
||||
|
||||
#[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<"));
|
||||
fn typed_pid_is_a_copyable_identity() {
|
||||
let p = Pid::<Counter>::from_raw(RawPid::new(3, 1));
|
||||
let q = p; // Copy, not move
|
||||
assert_eq!(p.index(), 3);
|
||||
assert_eq!(p.generation(), 1);
|
||||
assert_eq!(p, q);
|
||||
// Same index, different generation = different incarnation.
|
||||
assert_ne!(p, Pid::<Counter>::from_raw(RawPid::new(3, 2)));
|
||||
assert!(format!("{p:?}").starts_with("Pid("));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn erase_drops_the_type_but_keeps_identity() {
|
||||
let p = Pid::<Counter>::from_raw(RawPid::new(7, 4));
|
||||
assert_eq!(p.erase(), Pid::new(7, 4)); // Pid<Erased>
|
||||
assert_eq!(p.raw(), RawPid::new(7, 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -212,12 +258,12 @@ mod typed_addr_tests {
|
||||
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.
|
||||
// Identity tokens must be usable across threads.
|
||||
#[test]
|
||||
fn tokens_are_send_sync_without_key_bounds() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<Addr<Counter>>();
|
||||
assert_send_sync::<Pid<Counter>>();
|
||||
assert_send_sync::<Pid<Erased>>();
|
||||
assert_send_sync::<Name<CounterMsg>>();
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -175,7 +175,8 @@ pub fn spawn(f: impl FnOnce() + Send + 'static) -> JoinHandle {
|
||||
spawn_under(parent, f)
|
||||
}
|
||||
|
||||
pub fn spawn_under(supervisor: Pid, f: impl FnOnce() + Send + 'static) -> JoinHandle {
|
||||
pub fn spawn_under<A>(supervisor: Pid<A>, f: impl FnOnce() + Send + 'static) -> JoinHandle {
|
||||
let supervisor = supervisor.erase();
|
||||
// Stack + closure boxing happen before ANY runtime lock is taken: no
|
||||
// syscall and no allocation ever stalls another scheduler thread.
|
||||
let stack = with_runtime(|inner| inner.stack_pool.lock().pop())
|
||||
@@ -287,7 +288,8 @@ pub(crate) fn retire_wait() {
|
||||
/// observation point — a tight loop with no `check!()`, no allocation, and no
|
||||
/// blocking op — cannot be stopped, exactly as it cannot be preempted. A no-op
|
||||
/// if `pid` is already gone.
|
||||
pub fn request_stop(pid: Pid) {
|
||||
pub fn request_stop<A>(pid: Pid<A>) {
|
||||
let pid = pid.erase();
|
||||
let _ = try_with_runtime(|inner| {
|
||||
if let Some(slot) = inner.slot_at(pid) {
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user