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:
+111
-1
@@ -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"]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user