feat(gen_server): handle_info — static info arms selected ahead of the inbox

type Info + handle_info (no-op default) on GenServer; ServerBuilder
(with_info/under/start) so start variants don't multiply — start/start_under
stay as wrappers. The loop selects [infos.., inbox] in priority order when
info arms exist, and keeps the plain recv() park when none do. Closed info
arms are silently removed (closed-arm-is-ready-forever); a closed inbox
still means graceful shutdown.
This commit is contained in:
Claude
2026-06-10 14:56:51 +00:00
parent e545f818fa
commit e5d1b3b54b
3 changed files with 250 additions and 33 deletions
+111 -1
View File
@@ -1,7 +1,7 @@
//! gen_server tests: call round-trip, cast, lifecycle callbacks, and the two
//! server-down detection paths (reply-channel close vs. inbox-send failure).
use smarm::gen_server::{start, CallError, GenServer};
use smarm::gen_server::{start, CallError, GenServer, ServerBuilder};
use smarm::run;
use std::sync::{Arc, Mutex};
@@ -26,6 +26,7 @@ impl GenServer for Counter {
type Call = Req;
type Reply = i64;
type Cast = Op;
type Info = ();
fn handle_call(&mut self, req: Req) -> i64 {
match req {
@@ -68,6 +69,7 @@ impl GenServer for Lifecycle {
type Call = ();
type Reply = ();
type Cast = ();
type Info = ();
fn init(&mut self) {
self.log.lock().unwrap().push("init");
@@ -151,6 +153,7 @@ impl GenServer for Slow {
type Call = u64; // delay in ms
type Reply = u64;
type Cast = ();
type Info = ();
fn handle_call(&mut self, delay_ms: u64) -> u64 {
if delay_ms > 0 {
@@ -206,6 +209,7 @@ fn call_timeout_to_dead_server_is_server_down_not_timeout() {
struct Bomb;
impl GenServer for Bomb {
type Call = ();
type Info = ();
type Reply = ();
type Cast = ();
fn handle_call(&mut self, _: ()) {
@@ -229,3 +233,109 @@ fn call_timeout_to_dead_server_is_server_down_not_timeout() {
);
});
}
// ---------------------------------------------------------------------------
// handle_info: out-of-band channels selected alongside the inbox (v0.8)
// ---------------------------------------------------------------------------
/// Logs every message it handles, in order; a call reads the log back.
struct Logger {
log: Vec<&'static str>,
}
impl GenServer for Logger {
type Call = ();
type Reply = Vec<&'static str>;
type Cast = ();
type Info = &'static str;
fn handle_call(&mut self, _: ()) -> Vec<&'static str> {
self.log.clone()
}
fn handle_cast(&mut self, _: ()) {
self.log.push("cast");
}
fn handle_info(&mut self, info: &'static str) {
self.log.push(info);
}
}
// An info message is dispatched to handle_info, interleaved with normal
// service.
#[test]
fn info_is_dispatched() {
let got = Arc::new(Mutex::new(Vec::new()));
let got2 = got.clone();
run(move || {
let (info_tx, info_rx) = smarm::channel::<&'static str>();
let server = ServerBuilder::new(Logger { log: Vec::new() })
.with_info(info_rx)
.start();
info_tx.send("info").unwrap();
*got2.lock().unwrap() = server.call(()).unwrap();
});
assert_eq!(*got.lock().unwrap(), vec!["info"]);
}
// Arm priority: with a cast AND an info both queued before the server first
// runs, the info is handled first — info arms outrank the inbox. Relies on
// run()'s deterministic single-thread ordering.
#[test]
fn info_outranks_inbox() {
let got = Arc::new(Mutex::new(Vec::new()));
let got2 = got.clone();
run(move || {
let (info_tx, info_rx) = smarm::channel::<&'static str>();
let server = ServerBuilder::new(Logger { log: Vec::new() })
.with_info(info_rx)
.start();
// The server actor hasn't run yet: both messages are queued before
// its first select. Inbox first in *send* order, info first in *arm*
// order — arm order must win.
server.cast(()).unwrap();
info_tx.send("info").unwrap();
*got2.lock().unwrap() = server.call(()).unwrap();
});
assert_eq!(*got.lock().unwrap(), vec!["info", "cast"]);
}
// Two info channels: declaration order is priority order.
#[test]
fn info_arms_keep_declaration_priority() {
let got = Arc::new(Mutex::new(Vec::new()));
let got2 = got.clone();
run(move || {
let (hi_tx, hi_rx) = smarm::channel::<&'static str>();
let (lo_tx, lo_rx) = smarm::channel::<&'static str>();
let server = ServerBuilder::new(Logger { log: Vec::new() })
.with_info(hi_rx)
.with_info(lo_rx)
.start();
// Sent low-priority first; handled high-priority first.
lo_tx.send("lo").unwrap();
hi_tx.send("hi").unwrap();
*got2.lock().unwrap() = server.call(()).unwrap();
});
assert_eq!(*got.lock().unwrap(), vec!["hi", "lo"]);
}
// A closed info arm is silently dropped and the server keeps serving; the
// closure does NOT reach handle_info and does NOT starve the inbox (the
// closed-arm-is-ready-forever gotcha).
#[test]
fn closed_info_arm_is_dropped_silently() {
let got = Arc::new(Mutex::new(Vec::new()));
let got2 = got.clone();
run(move || {
let (info_tx, info_rx) = smarm::channel::<&'static str>();
let server = ServerBuilder::new(Logger { log: Vec::new() })
.with_info(info_rx)
.start();
drop(info_tx); // closed before the server's first select
server.cast(()).unwrap();
*got2.lock().unwrap() = server.call(()).unwrap();
});
assert_eq!(*got.lock().unwrap(), vec!["cast"]);
}