gen_server: drop-guard drains live timers + debug_assert no leak (RFC 015 §4.7)

This commit is contained in:
smarm-agent
2026-06-18 19:19:15 +00:00
parent 8c8af55928
commit 1df85e2384
2 changed files with 57 additions and 13 deletions
+33 -13
View File
@@ -708,10 +708,30 @@ fn server_loop<G: GenServer>(
mut infos: Vec<Receiver<G::Info>>,
) {
// 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: GenServer>(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: GenServer>(G, Arc<Mutex<TimerReg<G>>>);
impl<G: GenServer> Drop for Terminate<G> {
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<G: GenServer>(
}
}
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::<Sys<G>>();
// 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::<Sys<G>>();
// 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).