gen_server: idle/receive timeout via select_timeout/recv_timeout + handle_idle (RFC 015 §4.4)

This commit is contained in:
smarm-agent
2026-06-18 19:17:20 +00:00
parent c0cfa01f37
commit 8c8af55928
2 changed files with 159 additions and 15 deletions
+66
View File
@@ -580,3 +580,69 @@ fn cancel_stops_a_periodic() {
// The periodic had fired at least once before being cancelled.
assert!(!fired.lock().unwrap().is_empty());
}
// ---------------------------------------------------------------------------
// RFC 015 §4.4 — idle / receive timeout. A server that sets an idle window in
// init and counts handle_idle fires; casts are traffic that resets the window.
// ---------------------------------------------------------------------------
struct Idler {
window: Duration,
idles: Arc<Mutex<u32>>,
}
impl GenServer for Idler {
type Call = ();
type Reply = u32; // idle fire count
type Cast = (); // a poke: traffic that resets the idle window
type Info = ();
type Timer = ();
fn init(&mut self, ctx: &ServerCtx<Self>) {
ctx.idle_after(self.window);
}
fn handle_call(&mut self, _: ()) -> u32 {
*self.idles.lock().unwrap()
}
fn handle_cast(&mut self, _: ()) {}
fn handle_idle(&mut self) {
*self.idles.lock().unwrap() += 1;
}
}
// A quiet inbox fires handle_idle, and the window re-arms (steady detector):
// several fires across a quiet span.
#[test]
fn idle_fires_repeatedly_on_quiet() {
let idles = Arc::new(Mutex::new(0));
let i2 = idles.clone();
run(move || {
let server = start(Idler { window: Duration::from_millis(25), idles: i2 });
smarm::sleep(Duration::from_millis(130)); // quiet ⇒ ~5 windows
drop(server); // keep the server alive across the quiet span
});
assert!(*idles.lock().unwrap() >= 2, "idle should re-arm and fire several times");
}
// Traffic within the window keeps idle from firing; only once the inbox goes
// quiet does handle_idle fire.
#[test]
fn traffic_resets_the_idle_window() {
let idles = Arc::new(Mutex::new(0));
let i2 = idles.clone();
let before_quiet = Arc::new(Mutex::new(u32::MAX));
let bq = before_quiet.clone();
run(move || {
let server = start(Idler { window: Duration::from_millis(60), idles: i2 });
// Poke every 25ms (< 60ms window) for ~100ms: each cast resets the
// window before it can elapse.
for _ in 0..4 {
server.cast(()).unwrap();
smarm::sleep(Duration::from_millis(25));
}
*bq.lock().unwrap() = server.call(()).unwrap(); // count while traffic kept it quiet-free
smarm::sleep(Duration::from_millis(140)); // now genuinely quiet
drop(server);
});
assert_eq!(*before_quiet.lock().unwrap(), 0, "steady traffic must suppress idle");
assert!(*idles.lock().unwrap() >= 1, "idle fires once the inbox falls quiet");
}