Tree snapshot of d9c62a8 (2026-08-18). The 20 source commits between
16ef583 (c9) and d9c62a8 were never pushed and the clone that held them
was lost; this commit carries their combined tree verbatim so the build
history stays auditable from the c1–c9 commits below it. Original
hashes as recorded in the session handoff:
c10 f03e94d pid targeting + auto-serialization (RemotePid, D14 name
on the wire); Phase 3 gate
c11 7ef4bad DownReason::Disconnected, wire tag 5
c12 d124162 remote monitors (Monitor/Demonitor/Down frames)
c13 9de967b connection-loss synthesis (A+B: Monitors::teardown +
unread-command Disconnected); Phase 4 gate
c14 7e822b7 eager pg eviction (reaper actor, ReaperInboxes)
dbe1a22 InboundVerdict::label(), trace::Event::ClusterInbound
31a9877 tests/channel.rs monitor-churn target gated on `go`
653559e Discovery::Withdrawn{name, addr}
c15 b41d76e distributed pg: Sync on NodeUp, Join/Leave broadcast,
NodeDown sweep, members_all; PgMsg wire type
c16 fafa881 pick_any / dispatch_any; Phase 5 complete
Phase 6 Tier A:
195c73e p4 NodeEvent::NodeDown(NodeInfo)
48fd766 p1 connector Candidate{name, addr, state}
ce8cf99 p2+p7 conn.rs select arms as Vec<Arm>; Outbound::Drained
bf24988 p6 RemotePid::from_local -> Option
9ae0380 p3 PeerStanding{Free, Claimed, Dialing}
c7d62a1 p11 cluster::Timing knobs, threaded by value
46f171d p11 cluster_disconnect un-ignored on SMARM_FAST_TIMING
Phase 6 Tier B:
391a9ae p5 cluster::RemoteDownReason{Local, Disconnected};
DownReason::Disconnected removed from core
7ddd908 p9 pg ctl channel unconditional, one cfg seam at spawn
d9c62a8 PeerNameMismatch parks the candidate; ClusterDial trace
Verified at d9c62a8: default 361/0, cluster 448/0, clippy --lib on
default / cluster / cluster+smarm-trace, fmt, 10x flake on
cluster_dial_mismatch, 5x on cluster_pg.
Reframe the module doc in the gen_server house style: lead with what a
process group is and when to reach for one, the auto-eviction-on-death
behaviour, groups-vs-registry, and a running-context note. Keep the
runnable example; add an ignored dispatch/worker-pool example.
Move implementation reasoning to where a maintainer stands: lock
discipline onto the ProcessGroups store, the eager-cleanup rationale onto
reap_group, the join finalize-race detail into join's body comment.
Drop all RFC references and the stray phase marker, and lead the public
read/select fn docs with what the caller gets. Demote the pub(crate)
assert_type intra-doc link in pick_as to plain code, clearing a
pre-existing broken-link warning.
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.
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.
A plain-actor example showing the one thing process groups are for: a live
membership view that self-heals on death. Two workers join a group; one dies
and is evicted by the death hook with no deregistration call; one leaves
voluntarily. Runs under cargo test --doc.
(Deliberately not a worker-pool-with-dispatch example: pg is membership and
liveness, not a message router — routing a job to a picked pid needs a
pid->handle map that pg does not provide, so a dispatch example would document
the adapter, not pg.)
members/pick now filter through a lock-free, generation-checked slot-word
read (live(), identical to the registry's guard) so a member that is already
dead but whose Down has not yet been drained is never *returned*. Eviction
stays the monitor's job (reap_group); this is the belt-and-braces read guard.
- members_where / first_member_where take an is_live oracle and replace the
unconditional first_member; members_of becomes test-only (raw enumeration).
- Unit test: the backstop hides a member the monitor has not yet reaped, and
does not itself evict (storage still holds it until reap).
Full suite green, warning-free.
Wire eviction to actor death via the monitor subsystem, using drain-on-
contact (no scheduler hot-path hook).
- Each membership now carries its own Monitor alongside the group entry;
join() installs monitor(pid) (registration races finalize_actor under the
cold lock, as every monitor does) and a redundant join tears its extra
monitor back down.
- reap_group: every group op drains the touched group's monitors with a
non-blocking try_recv (a delivered Down or closed channel = dead) and, on
the first death, sweeps that pid out of *every* group via remove_where —
so a death vanishes from all its groups on next contact.
- Lock discipline: monitor()/demonitor() (cold = Leaf) run outside the group
lock; try_recv (Channel) runs under it, permitted by the Leaf->Channel
order; evicted/rejected monitors are dropped after the lock releases so no
receiver-drop wakeup runs under it. Validated by the debug-build lock-order
checker passing under the integration tests.
- remove_where now returns the evicted monitors (for deferred drop) and
preserves intra-group insertion order.
- Tests: unit (synthetic monitors) for reap live/dead/closed + sweep;
integration (real actors) for death-vanishes-from-every-group, pick on an
all-dead group, peer survival, leave isolation, dead-at-join eviction.
Outstanding: a miri pass (cargo +nightly miri test --lib pg::) is untried —
first build exceeds the sandbox window and the futex RawMutex may need a miri
shim; the mechanical Leaf->Channel checker is the primary soundness guard.
Add a process-groups module (name -> multiset<Member>) sibling to the
registry, plus the cluster-shaped identity it is keyed on.
- NodeId/Incarnation u32 newtypes; Member { node, incarnation, pid } at
final (BEAM NEW_PID_EXT-shaped) layout.
- Config::node_id/incarnation builder setters beside wake_slot, defaulting
to a fixed single-node value; threaded through RuntimeInner::new.
- process_groups: RawMutex<ProcessGroups> on RuntimeInner, Leaf class,
mirroring the registry field.
- remove_where: the one dumb predicate-eviction primitive (no liveness wired
yet; first caller is the Phase 2 death hook).
- Public surface (join/leave/members/pick) in pre-liveness/pre-monitor form,
Pid-shaped; node/incarnation filled from runtime identity.
- Structural unit tests on synthetic members: idempotent join, multiset
semantics across groups, generation-distinct members, leave + empty-group
pruning, predicate sweep, incarnation-sweep shape.