feat(gen_server): handle_down — runtime monitor forwarding via ServerCtx/Watcher
Monitors are created at runtime (you watch a worker you just spawned in a handler), so down arms can't ride the static info list. init grows a &ServerCtx parameter (breaking; no-op default) whose clonable Watcher hands Monitors to the loop over a control arm; the loop selects the live down arms ahead of everything else. Arm priority: downs → control → infos → inbox. A delivered Down retires its arm (monitors are one-shot); a state that never clones the Watcher closes the control arm after init and the loop falls back to the plain-inbox park.
This commit is contained in:
+99
-1
@@ -71,7 +71,7 @@ impl GenServer for Lifecycle {
|
||||
type Cast = ();
|
||||
type Info = ();
|
||||
|
||||
fn init(&mut self) {
|
||||
fn init(&mut self, _ctx: &smarm::gen_server::ServerCtx) {
|
||||
self.log.lock().unwrap().push("init");
|
||||
}
|
||||
|
||||
@@ -339,3 +339,101 @@ fn closed_info_arm_is_dropped_silently() {
|
||||
});
|
||||
assert_eq!(*got.lock().unwrap(), vec!["cast"]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// handle_down: monitors handed to the loop via Watcher (v0.8)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use smarm::gen_server::Watcher;
|
||||
use smarm::{monitor, spawn, DownReason, Pid};
|
||||
|
||||
/// The motivating pattern: a server that spawns workers from a handler,
|
||||
/// watches them, and logs their deaths.
|
||||
struct Pool {
|
||||
watcher: Option<Watcher>,
|
||||
log: Vec<DownReason>,
|
||||
}
|
||||
|
||||
enum PoolCast {
|
||||
SpawnDoomedWorker,
|
||||
Watch(Pid),
|
||||
}
|
||||
|
||||
impl GenServer for Pool {
|
||||
type Call = ();
|
||||
type Reply = Vec<DownReason>;
|
||||
type Cast = PoolCast;
|
||||
type Info = ();
|
||||
|
||||
fn init(&mut self, ctx: &smarm::gen_server::ServerCtx) {
|
||||
self.watcher = Some(ctx.watcher());
|
||||
}
|
||||
|
||||
fn handle_call(&mut self, _: ()) -> Vec<DownReason> {
|
||||
self.log.clone()
|
||||
}
|
||||
|
||||
fn handle_cast(&mut self, cast: PoolCast) {
|
||||
let watcher = self.watcher.as_ref().expect("init ran first");
|
||||
match cast {
|
||||
PoolCast::SpawnDoomedWorker => {
|
||||
let h = spawn(|| panic!("worker died"));
|
||||
watcher.watch(monitor(h.pid()));
|
||||
}
|
||||
PoolCast::Watch(pid) => watcher.watch(monitor(pid)),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_down(&mut self, down: smarm::Down) {
|
||||
self.log.push(down.reason);
|
||||
}
|
||||
}
|
||||
|
||||
// A worker spawned and watched from inside a handler delivers its Down to
|
||||
// handle_down. Down arms outrank the inbox, so the death is in the log by
|
||||
// the time the follow-up call is answered.
|
||||
#[test]
|
||||
fn worker_pool_down_reaches_handle_down() {
|
||||
let got = Arc::new(Mutex::new(Vec::new()));
|
||||
let got2 = got.clone();
|
||||
run(move || {
|
||||
let server = start(Pool { watcher: None, log: Vec::new() });
|
||||
server.cast(PoolCast::SpawnDoomedWorker).unwrap();
|
||||
let _ = server.call(()).unwrap(); // sync point: cast handled, worker live
|
||||
*got2.lock().unwrap() = server.call(()).unwrap();
|
||||
});
|
||||
assert_eq!(*got.lock().unwrap(), vec![DownReason::Panic]);
|
||||
}
|
||||
|
||||
// Watching an already-dead pid yields an immediate NoProc Down, and the down
|
||||
// arm outranks the inbox: the call cast *after* the watch still observes it.
|
||||
#[test]
|
||||
fn watch_dead_pid_is_noproc_down() {
|
||||
let got = Arc::new(Mutex::new(Vec::new()));
|
||||
let got2 = got.clone();
|
||||
run(move || {
|
||||
let h = spawn(|| {});
|
||||
let dead = h.pid();
|
||||
h.join().unwrap();
|
||||
let server = start(Pool { watcher: None, log: Vec::new() });
|
||||
server.cast(PoolCast::Watch(dead)).unwrap();
|
||||
*got2.lock().unwrap() = server.call(()).unwrap();
|
||||
});
|
||||
assert_eq!(*got.lock().unwrap(), vec![DownReason::NoProc]);
|
||||
}
|
||||
|
||||
// A state that never clones the Watcher closes the control arm; the loop
|
||||
// falls back to the plain-inbox park and keeps serving. (Every pre-v0.8 test
|
||||
// in this file also exercises this path.)
|
||||
#[test]
|
||||
fn unused_ctx_closes_control_arm_silently() {
|
||||
let got = Arc::new(Mutex::new(0i64));
|
||||
let got2 = got.clone();
|
||||
run(move || {
|
||||
let server = start(Counter { n: 0 });
|
||||
server.cast(Op::Add(2)).unwrap();
|
||||
server.cast(Op::Add(40)).unwrap();
|
||||
*got2.lock().unwrap() = server.call(Req::Get).unwrap();
|
||||
});
|
||||
assert_eq!(*got.lock().unwrap(), 42);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user