mailbox: typed-path producers, by-name gen_servers, root-exit teardown (RFC 014)
Implements the new addressing surface the examples in examples/ specify: - spawn_addr::<A>(FnOnce(Receiver<A::Msg>)) -> Pid<A>: the typed-path producer. Makes the inbox, spawns the body with its receiver, and publishes the sender from the PARENT side (registry::install_for) before returning the pid, so an immediate send_to always resolves (no race on the body installing itself). Detached, like ServerBuilder::start. - lookup_as / pick_as / members_as: re-type an erased pid as Pid<A> over the heterogeneous registry/pg stores, sharing one helper (pid::assert_type). Unchecked but sound: routing is by message TypeId, so a wrong A degrades to SendError::NoChannel on the next send, never a misdelivery. - dispatch::<A>(group, msg) -> Result<Pid<A>, SendError<A::Msg>>: pick_as + send_to, with the message handed back on failure. New SendError::NoMember variant for the empty/all-dead group case. - gen_server naming: ServerName<G> backed internally by the registry's existing typed-channel store keyed by TypeId::of::<Envelope<G>>() — Envelope stays private, no separate directory. Type-state NamedServerBuilder<G> keeps the current infallible ServerBuilder::start() untouched; its start() is fallible (parent-side register, NameTaken). Free call/cast/whereis_server resolve per use. ServerRef::shutdown (+ free shutdown) is the sys-style synchronous stop. - root-exit teardown: the run's initial actor is recorded as root; when it finalizes it flags root_exited, and the scheduler's idle verdict then stops the parked-forever remainder (a one-shot RootDrain sweep). Deferred to the idle point on purpose: the run queue drains first, so actors with queued work finish rather than unwinding on the stop. This lets a named-server daemon (or any pinned actor) wind the run down instead of hanging on live_actors > 0. request_stop is refactored to a request_stop_inner core so the sweep can drive it from inside the runtime without re-borrowing the thread-local. Lib + tests + examples build warning-free; full suite green.
This commit is contained in:
+46
-12
@@ -195,6 +195,33 @@ pub fn spawn_under<A>(supervisor: Pid<A>, f: impl FnOnce() + Send + 'static) ->
|
||||
JoinHandle { pid, consumed: false }
|
||||
}
|
||||
|
||||
/// Spawn a typed, single-message actor and hand back its identity-bound
|
||||
/// [`Pid<A>`] (RFC 014's typed-path producer). The runtime makes the actor's
|
||||
/// inbox, hands the body its [`Receiver<A::Msg>`], installs the sender, and
|
||||
/// returns the parent a `Pid<A>`.
|
||||
///
|
||||
/// The inbox is published from the parent side **before** the pid is returned
|
||||
/// (see [`registry::install_for`](crate::registry::install_for)), so the address
|
||||
/// is live the instant the caller holds it: an immediate
|
||||
/// [`send_to`](crate::send_to) always resolves, never racing the body's first
|
||||
/// instruction. The actor is detached — its lifetime is governed by its own
|
||||
/// logic (an explicit stop message, or returning), like
|
||||
/// [`ServerBuilder::start`](crate::ServerBuilder::start) — so the backing join
|
||||
/// handle is dropped. Spawns under the current actor (via [`spawn`]).
|
||||
///
|
||||
/// Panics if called outside `Runtime::run()`.
|
||||
pub fn spawn_addr<A: crate::pid::Addressable>(
|
||||
body: impl FnOnce(crate::channel::Receiver<A::Msg>) + Send + 'static,
|
||||
) -> Pid<A> {
|
||||
let (tx, rx) = crate::channel::channel::<A::Msg>();
|
||||
let handle = spawn(move || body(rx));
|
||||
let pid = handle.pid();
|
||||
// Publish the sender for `pid` before returning the typed address. `handle`
|
||||
// drops at end of scope (detached).
|
||||
crate::registry::install_for::<A::Msg>(pid, tx);
|
||||
crate::pid::assert_type::<A>(pid)
|
||||
}
|
||||
|
||||
use crate::context::init_actor_stack;
|
||||
|
||||
pub fn self_pid() -> Pid {
|
||||
@@ -290,21 +317,28 @@ pub(crate) fn retire_wait() {
|
||||
/// if `pid` is already gone.
|
||||
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) {
|
||||
{
|
||||
let cold = slot.cold.lock();
|
||||
// Verify under the cold lock: generation can't change while
|
||||
// we hold it (reclaim takes the same lock).
|
||||
if slot.generation() == pid.generation() {
|
||||
if let Some(actor) = cold.actor.as_ref() {
|
||||
actor.stop.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
let _ = try_with_runtime(|inner| request_stop_inner(inner, pid));
|
||||
}
|
||||
|
||||
/// The core of [`request_stop`], taking the runtime directly so it can be
|
||||
/// driven from inside the runtime (e.g. the root-exit sweep in
|
||||
/// `finalize_actor`) without re-borrowing the thread-local. Sets the stop flag
|
||||
/// under the target's cold lock (generation re-verified there; a mismatch or a
|
||||
/// slot with no live actor is a no-op) and wakes it.
|
||||
pub(crate) fn request_stop_inner(inner: &RuntimeInner, pid: Pid) {
|
||||
if let Some(slot) = inner.slot_at(pid) {
|
||||
{
|
||||
let cold = slot.cold.lock();
|
||||
// Verify under the cold lock: generation can't change while
|
||||
// we hold it (reclaim takes the same lock).
|
||||
if slot.generation() == pid.generation() {
|
||||
if let Some(actor) = cold.actor.as_ref() {
|
||||
actor.stop.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
inner.unpark(pid);
|
||||
}
|
||||
});
|
||||
inner.unpark(pid);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user