From 1df85e2384170efd1275045cd73b690c47d72c56 Mon Sep 17 00:00:00 2001 From: smarm-agent Date: Thu, 18 Jun 2026 19:19:15 +0000 Subject: [PATCH] =?UTF-8?q?gen=5Fserver:=20drop-guard=20drains=20live=20ti?= =?UTF-8?q?mers=20+=20debug=5Fassert=20no=20leak=20(RFC=20015=20=C2=A74.7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gen_server.rs | 46 ++++++++++++++++++++++++++++++++------------- tests/gen_server.rs | 24 +++++++++++++++++++++++ 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/src/gen_server.rs b/src/gen_server.rs index aec901f..04593c9 100644 --- a/src/gen_server.rs +++ b/src/gen_server.rs @@ -708,10 +708,30 @@ fn server_loop( mut infos: Vec>, ) { // terminate() must run on every exit path (clean close, panic, stop), so it - // lives in this guard's Drop rather than after the loop. - struct Terminate(G); + // lives in this guard's Drop rather than after the loop. The guard also owns + // a registry handle so the *same* drop drains every live timer (RFC 015 + // §4.7): once the loop is gone no periodic can re-arm anyway, but cancelling + // here keeps no armed substrate entry lingering past the server, and the + // assert pins the invariant. + struct Terminate(G, Arc>>); impl Drop for Terminate { fn drop(&mut self) { + { + let mut reg = self.1.lock().unwrap(); + for (_, sub) in reg.oneshots.drain() { + cancel_timer(sub); + } + for (_, p) in reg.periodics.drain() { + cancel_timer(p.live); + } + reg.rearm_tx = None; + // A fired-but-gone send already no-ops on the closed Sys channel; + // this guards the armed-but-unfired re-arming case specifically. + debug_assert!( + reg.oneshots.is_empty() && reg.periodics.is_empty(), + "an armed timer survived gen_server loop exit" + ); + } self.0.terminate(); } } @@ -728,18 +748,18 @@ fn server_loop( } } - let mut guard = Terminate(state); - - // The system intake arm: Watchers feed Monitors and (next chunk) armed - // timers feed fires to the loop through it. The ctx (and with it the loop's - // own sender) drops right after init — a state that didn't clone the - // Watcher closes the arm, the first select observes the closure, and the - // loop falls back to the plain-inbox park. - let (sys_tx, sys_rx) = channel::>(); - // Shared timer bookkeeping: the loop keeps one clone (to retire one-shots - // on fire, and to drain on exit); every TimerHandle the state stores holds - // another. + // Shared timer bookkeeping: the loop keeps one clone (to retire one-shots on + // fire, re-arm periodics, and read the idle window); the guard holds another + // to drain on exit; every TimerHandle the state stores holds more. let reg = Arc::new(Mutex::new(TimerReg::default())); + let mut guard = Terminate(state, reg.clone()); + + // The system intake arm: Watchers feed Monitors and armed timers feed fires + // to the loop through it. The ctx (and with it the loop's own sender) drops + // right after init — a state that cloned no Watcher/TimerHandle closes the + // arm, the first select observes the closure, and the loop falls back to the + // plain-inbox park. + let (sys_tx, sys_rx) = channel::>(); // Bind the ctx so the idle window set during init can be read back, then // drop it — that drops the loop's own Sys sender, so a state that cloned no // Watcher/TimerHandle lets the arm auto-close (the unused-ctx behaviour). diff --git a/tests/gen_server.rs b/tests/gen_server.rs index 169466b..80c8405 100644 --- a/tests/gen_server.rs +++ b/tests/gen_server.rs @@ -646,3 +646,27 @@ fn traffic_resets_the_idle_window() { assert_eq!(*before_quiet.lock().unwrap(), 0, "steady traffic must suppress idle"); assert!(*idles.lock().unwrap() >= 1, "idle fires once the inbox falls quiet"); } + +// RFC 015 §4.7 — no armed timer survives loop exit. A server with a live +// periodic is dropped; the loop's drop guard drains and cancels it (a surviving +// re-arming timer would trip the in-Drop debug_assert), runs terminate, and no +// further tick lands after exit. +#[test] +fn no_timer_survives_exit() { + let fired = Arc::new(Mutex::new(Vec::new())); + let f_server = fired.clone(); + let f_read = fired.clone(); + run(move || { + let server = start(timed(f_server, Arc::new(Mutex::new(None)))); + server.cast(TkCast::Tick(Duration::from_millis(15))).unwrap(); + let _ = server.call(()).unwrap(); // sync: periodic armed + smarm::sleep(Duration::from_millis(45)); // a couple of ticks + let mon = smarm::monitor(server.pid()); + drop(server); // inbox closes → loop exits → guard drains timers + // Clean Down ⇒ the loop returned without the no-leak assert aborting. + assert!(mon.rx.recv().is_ok()); + let at_exit = f_read.lock().unwrap().len(); + smarm::sleep(Duration::from_millis(90)); // would be several more ticks + assert_eq!(f_read.lock().unwrap().len(), at_exit, "no tick may fire after exit"); + }); +}