pg: Phase 2 — liveness & the monitor death hook (RFC 012)
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.
This commit is contained in:
+131
@@ -0,0 +1,131 @@
|
||||
//! 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.
|
||||
//! (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 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.
|
||||
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");
|
||||
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 reaped");
|
||||
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_is_evicted_on_next_contact() {
|
||||
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("late", pid);
|
||||
assert!(members("late").is_empty(), "dead-at-join member is reaped on read");
|
||||
assert_eq!(pick("late"), None);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user