feat(monitor): unidirectional one-shot process monitors
`monitor(pid) -> Receiver<Down>` lets any actor watch any other and
receive a single Down{pid, reason} when it terminates. Generalizes the
existing single supervisor_channel (which is just a hard-wired monitor
the parent installs at spawn).
- src/monitor.rs: Down / DownReason{Exit,Panic,NoProc} + monitor().
Payload-free: a panic payload has one owner and goes to the joiner via
JoinError, so monitors learn only that it panicked. Monitoring an
already-gone pid yields NoProc immediately.
- runtime: Slot grows `monitors: Vec<Sender<Down>>`; finalize_actor
drains and notifies them outside the shared lock (send can unpark a
receiver, which re-takes the non-reentrant mutex). Cleared in
reclaim_slot and on slot reuse at spawn.
- Registration and finalize both serialize on the shared mutex, so a
target alive at registration always delivers a real Down (no race
between liveness check and registration).
tests/monitor.rs: Exit, Panic, NoProc-on-dead-target, and fan-out to
multiple monitors. Full suite green.
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
//! Monitor tests: `monitor(pid)` delivers exactly one `Down` describing how
|
||||
//! the target terminated.
|
||||
//!
|
||||
//! A monitor is unidirectional and one-shot: it never propagates failure to
|
||||
//! the watcher (that's a link), it just drops a `Down` into the returned
|
||||
//! channel. These tests pin the three reasons — `Exit`, `Panic`, `NoProc` —
|
||||
//! and the fan-out case where several monitors watch the same target.
|
||||
//!
|
||||
//! Scheduling note: under the default single-thread runtime the parent runs
|
||||
//! until it parks, so every `monitor()` call below registers while the target
|
||||
//! is still `Runnable` (it hasn't been resumed yet). Registration and
|
||||
//! `finalize_actor` both serialize on the shared mutex, so a target that is
|
||||
//! alive at registration time always produces a real `Down`, never a missed
|
||||
//! one.
|
||||
|
||||
use smarm::{monitor, run, spawn, DownReason};
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn monitor_sees_normal_exit() {
|
||||
let ok = Arc::new(AtomicBool::new(false));
|
||||
let o = ok.clone();
|
||||
run(move || {
|
||||
let h = spawn(|| {});
|
||||
let pid = h.pid();
|
||||
let down = monitor(pid);
|
||||
let d = down.recv().expect("monitor channel closed before Down");
|
||||
assert_eq!(d.pid, pid, "Down reported the wrong pid");
|
||||
if matches!(d.reason, DownReason::Exit) {
|
||||
o.store(true, Ordering::SeqCst);
|
||||
}
|
||||
let _ = h.join();
|
||||
});
|
||||
assert!(ok.load(Ordering::SeqCst), "expected DownReason::Exit");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn monitor_sees_panic() {
|
||||
let ok = Arc::new(AtomicBool::new(false));
|
||||
let o = ok.clone();
|
||||
run(move || {
|
||||
let h = spawn(|| panic!("boom"));
|
||||
let pid = h.pid();
|
||||
let down = monitor(pid);
|
||||
let d = down.recv().expect("monitor channel closed before Down");
|
||||
assert_eq!(d.pid, pid);
|
||||
if matches!(d.reason, DownReason::Panic) {
|
||||
o.store(true, Ordering::SeqCst);
|
||||
}
|
||||
// Drain the panic outcome so the joiner path is exercised too; the
|
||||
// payload goes here, not to the monitor.
|
||||
let _ = h.join();
|
||||
});
|
||||
assert!(ok.load(Ordering::SeqCst), "expected DownReason::Panic");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn monitor_already_dead_target_is_noproc() {
|
||||
let ok = Arc::new(AtomicBool::new(false));
|
||||
let o = ok.clone();
|
||||
run(move || {
|
||||
let h = spawn(|| {});
|
||||
let pid = h.pid();
|
||||
// Consume the only handle on a finished child: the slot is reclaimed
|
||||
// and its generation bumped, so `pid` is now stale.
|
||||
h.join().unwrap();
|
||||
let down = monitor(pid);
|
||||
let d = down.recv().expect("NoProc Down should be delivered immediately");
|
||||
assert_eq!(d.pid, pid);
|
||||
if matches!(d.reason, DownReason::NoProc) {
|
||||
o.store(true, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
assert!(ok.load(Ordering::SeqCst), "expected DownReason::NoProc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_monitors_all_notified() {
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let c = count.clone();
|
||||
run(move || {
|
||||
let h = spawn(|| {});
|
||||
let pid = h.pid();
|
||||
let monitors = [monitor(pid), monitor(pid), monitor(pid)];
|
||||
let _ = h.join();
|
||||
for m in monitors {
|
||||
if matches!(m.recv().unwrap().reason, DownReason::Exit) {
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
});
|
||||
assert_eq!(count.load(Ordering::SeqCst), 3, "every monitor should see the Down");
|
||||
}
|
||||
Reference in New Issue
Block a user