feat(cluster): RFC 010 c10–c16, follow-ups and Phase 6 (squash of 16ef583..d9c62a8)

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.
This commit is contained in:
claude-asm-audit
2026-08-18 16:00:00 +02:00
parent 16ef583455
commit 4c0e42152f
32 changed files with 4202 additions and 578 deletions
+13 -20
View File
@@ -1,5 +1,6 @@
//! Process-group tests that run under the scheduler: `join` installs a real
//! monitor on a live actor, and a real death drives eviction on next contact.
//! monitor on a live actor, and a real death drives eviction (the reaper
//! actor sweeps it; the read path hides it in the meantime).
//! (Pure structural invariants live in the `pg` unit tests.)
use smarm::{channel, members, pick, run, spawn};
@@ -35,19 +36,15 @@ fn a_dead_actor_vanishes_from_every_group_it_joined() {
assert_eq!(members("g1"), vec![pid]);
assert_eq!(members("g2"), vec![pid]);
// Release and reap the actor. finalize_actor queues the Down to our
// monitors before unparking joiners, so by the time join() returns the
// Down is already waiting in the membership channel.
// Release the actor. finalize_actor queues the Down to the reaper and
// marks the slot dead before unparking joiners, so by the time join()
// returns every read hides the pid whether or not the reaper has run.
tx.send(()).unwrap();
h.join().unwrap();
// Drain-on-contact: touching g1 detects the death and sweeps the pid
// out of every group (g2 included), not just g1.
assert!(members("g1").is_empty(), "evicted from the touched group");
assert!(
members("g2").is_empty(),
"and swept from the untouched group"
);
// Gone from every group it joined, not just one.
assert!(members("g1").is_empty(), "gone from g1");
assert!(members("g2").is_empty(), "and from g2");
assert_eq!(pick("g1"), None);
});
}
@@ -86,11 +83,7 @@ fn live_members_survive_a_peers_death() {
tx_a.send(()).unwrap();
a.join().unwrap();
assert_eq!(
members("svc"),
vec![b.pid()],
"only the dead peer is reaped"
);
assert_eq!(members("svc"), vec![b.pid()], "only the dead peer is gone");
assert_eq!(pick("svc"), Some(b.pid()));
tx_b.send(()).unwrap();
@@ -123,18 +116,18 @@ fn leave_drops_a_membership_without_affecting_others() {
}
#[test]
fn joining_an_already_dead_pid_is_evicted_on_next_contact() {
fn joining_an_already_dead_pid_never_shows_in_a_read() {
run(|| {
let h = spawn(|| {});
let pid = h.pid();
h.join().unwrap(); // actor is finalized before we join it to anything
// monitor() on a gone pid queues a NoProc Down immediately, so the
// membership is reaped the next time the group is touched.
// join() on a gone pid queues a NoProc Down to the reaper immediately;
// reads never show it either way (slot-liveness backstop).
join("late", pid);
assert!(
members("late").is_empty(),
"dead-at-join member is reaped on read"
"dead-at-join member never reads as live"
);
assert_eq!(pick("late"), None);
});