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.
135 lines
4.0 KiB
Rust
135 lines
4.0 KiB
Rust
//! Process-group tests that run under the scheduler: `join` installs a real
|
|
//! 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};
|
|
use smarm::{join, leave};
|
|
|
|
#[test]
|
|
fn join_then_members_lists_a_live_member() {
|
|
run(|| {
|
|
let (tx, rx) = channel::<()>();
|
|
let h = spawn(move || {
|
|
rx.recv().unwrap();
|
|
});
|
|
let pid = h.pid();
|
|
assert!(join("workers", pid), "first join is new");
|
|
assert!(!join("workers", pid), "second join is idempotent");
|
|
assert_eq!(members("workers"), vec![pid]);
|
|
assert_eq!(pick("workers"), Some(pid));
|
|
tx.send(()).unwrap();
|
|
h.join().unwrap();
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn a_dead_actor_vanishes_from_every_group_it_joined() {
|
|
run(|| {
|
|
let (tx, rx) = channel::<()>();
|
|
let h = spawn(move || {
|
|
rx.recv().unwrap();
|
|
});
|
|
let pid = h.pid();
|
|
join("g1", pid);
|
|
join("g2", pid);
|
|
assert_eq!(members("g1"), vec![pid]);
|
|
assert_eq!(members("g2"), vec![pid]);
|
|
|
|
// 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();
|
|
|
|
// 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);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn pick_returns_none_when_the_only_member_is_dead() {
|
|
run(|| {
|
|
let (tx, rx) = channel::<()>();
|
|
let h = spawn(move || {
|
|
rx.recv().unwrap();
|
|
});
|
|
let pid = h.pid();
|
|
join("pool", pid);
|
|
tx.send(()).unwrap();
|
|
h.join().unwrap();
|
|
assert_eq!(pick("pool"), None);
|
|
assert!(members("pool").is_empty());
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn live_members_survive_a_peers_death() {
|
|
run(|| {
|
|
let (tx_a, rx_a) = channel::<()>();
|
|
let (tx_b, rx_b) = channel::<()>();
|
|
let a = spawn(move || {
|
|
rx_a.recv().unwrap();
|
|
});
|
|
let b = spawn(move || {
|
|
rx_b.recv().unwrap();
|
|
});
|
|
join("svc", a.pid());
|
|
join("svc", b.pid());
|
|
|
|
// Kill a; b is still parked on its channel.
|
|
tx_a.send(()).unwrap();
|
|
a.join().unwrap();
|
|
|
|
assert_eq!(members("svc"), vec![b.pid()], "only the dead peer is gone");
|
|
assert_eq!(pick("svc"), Some(b.pid()));
|
|
|
|
tx_b.send(()).unwrap();
|
|
b.join().unwrap();
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn leave_drops_a_membership_without_affecting_others() {
|
|
run(|| {
|
|
let (tx_a, rx_a) = channel::<()>();
|
|
let (tx_b, rx_b) = channel::<()>();
|
|
let a = spawn(move || {
|
|
rx_a.recv().unwrap();
|
|
});
|
|
let b = spawn(move || {
|
|
rx_b.recv().unwrap();
|
|
});
|
|
join("g", a.pid());
|
|
join("g", b.pid());
|
|
assert!(leave("g", a.pid()), "a was a member");
|
|
assert!(!leave("g", a.pid()), "leaving twice finds nothing");
|
|
assert_eq!(members("g"), vec![b.pid()]);
|
|
|
|
tx_a.send(()).unwrap();
|
|
tx_b.send(()).unwrap();
|
|
a.join().unwrap();
|
|
b.join().unwrap();
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
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
|
|
|
|
// 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 never reads as live"
|
|
);
|
|
assert_eq!(pick("late"), None);
|
|
});
|
|
}
|