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
+138 -31
View File
@@ -6,14 +6,23 @@
//! [`call`](ServerRef::call) (synchronous, returns a reply) or
//! [`cast`](ServerRef::cast) (fire-and-forget).
//!
//! ## Why a single inbox
//! ## One inbox, many arms
//!
//! smarm has no `select` and no unified per-process mailbox (see the runtime
//! gotchas in `task.md`), so a server cannot wait on a "call channel" and a
//! "cast channel" simultaneously. Instead every message — call or cast —
//! travels the *same* inbox channel as an [`Envelope`], and the server loop
//! dispatches by variant. A `call` carries a freshly made one-shot reply
//! channel; the server sends the reply straight back down it.
//! Every call and cast travels the *same* inbox channel as an [`Envelope`]
//! (calls and casts are ordered relative to each other, like Erlang); the
//! server loop dispatches by variant. A `call` carries a freshly made
//! one-shot reply channel; the server sends the reply straight back down it.
//!
//! Out-of-band messages ride *separate* channels composed at the wait via
//! [`select`](crate::channel::select): info channels handed over at start
//! ([`ServerBuilder::with_info`]) are dispatched to
//! [`handle_info`](GenServer::handle_info). Arm priority is
//! **infos before inbox**, in declaration order — a hot inbox cannot starve
//! an out-of-band message; conversely a hot info channel CAN starve the
//! inbox, deliberately (system-message semantics). An info channel whose
//! senders are all gone is silently dropped from the arm set (per the
//! closed-arm-is-ready-forever rule on `select`); a closed *inbox* still
//! means graceful shutdown.
//!
//! ## Server death
//!
@@ -36,11 +45,11 @@
//!
//! ## Not here (yet)
//!
//! No `handle_info`: it needs the cross-channel mailbox merge that selective
//! receive deliberately left unmade. (Call timeouts landed as
//! [`ServerRef::call_timeout`], on top of `Receiver::recv_timeout`.)
//! No dynamic info subscription: the info-channel set is fixed at start.
//! Revisit against a real consumer. (Monitor `Down` forwarding is dynamic —
//! see `handle_down` — because monitors are inherently created at runtime.)
use crate::channel::{channel, Receiver, Sender};
use crate::channel::{channel, select, Receiver, Selectable, Sender};
use crate::pid::Pid;
use crate::scheduler::{spawn, spawn_under};
@@ -56,6 +65,11 @@ pub trait GenServer: Send + 'static {
type Reply: Send + 'static;
/// Asynchronous request type (carried by [`ServerRef::cast`]).
type Cast: Send + 'static;
/// Out-of-band message type, delivered to [`handle_info`](Self::handle_info)
/// from the info channels registered at start
/// ([`ServerBuilder::with_info`]). Servers with several out-of-band
/// sources enum them up into one `Info`. Use `()` if unused.
type Info: Send + 'static;
/// Runs once inside the server actor before any message is handled.
fn init(&mut self) {}
@@ -66,6 +80,10 @@ pub trait GenServer: Send + 'static {
/// Handle a fire-and-forget cast.
fn handle_cast(&mut self, request: Self::Cast);
/// Handle an out-of-band message from one of the info channels. Default:
/// drop it.
fn handle_info(&mut self, _info: Self::Info) {}
/// Runs as the server actor exits, on any exit path (see module docs).
fn terminate(&mut self) {}
}
@@ -172,24 +190,72 @@ impl<G: GenServer> ServerRef<G> {
}
}
/// Configure-then-start construction for servers that need more than a bare
/// [`start`]: info channels, a supervisor. Consumed by [`start`](Self::start).
///
/// ```ignore
/// let server = ServerBuilder::new(state)
/// .with_info(events_rx)
/// .under(supervisor_pid)
/// .start();
/// ```
pub struct ServerBuilder<G: GenServer> {
state: G,
infos: Vec<Receiver<G::Info>>,
supervisor: Option<Pid>,
}
impl<G: GenServer> ServerBuilder<G> {
pub fn new(state: G) -> Self {
ServerBuilder { state, infos: Vec::new(), supervisor: None }
}
/// Add an out-of-band channel; messages arriving on it are dispatched to
/// [`GenServer::handle_info`]. Repeatable; arm priority is call order
/// (earlier = higher), and every info channel outranks the inbox.
pub fn with_info(mut self, rx: Receiver<G::Info>) -> Self {
self.infos.push(rx);
self
}
/// Spawn the server under an explicit supervisor pid (via [`spawn_under`])
/// so it slots into the supervision tree.
pub fn under(mut self, supervisor: Pid) -> Self {
self.supervisor = Some(supervisor);
self
}
/// Spawn the server actor and hand back its [`ServerRef`]. The server's
/// lifetime is governed by its refs, not by joining, so the backing join
/// handle is dropped.
pub fn start(self) -> ServerRef<G> {
let (tx, rx) = channel::<Envelope<G>>();
let ServerBuilder { state, infos, supervisor } = self;
let handle = match supervisor {
Some(sup) => spawn_under(sup, move || server_loop::<G>(rx, state, infos)),
None => spawn(move || server_loop::<G>(rx, state, infos)),
};
ServerRef { tx, pid: handle.pid() }
}
}
/// Spawn `state` as a server under the current actor (via [`spawn`]). Returns a
/// [`ServerRef`]; the server's lifetime is governed by its refs, not by
/// joining, so the backing join handle is dropped.
/// [`ServerRef`]. Shorthand for `ServerBuilder::new(state).start()`.
pub fn start<G: GenServer>(state: G) -> ServerRef<G> {
let (tx, rx) = channel::<Envelope<G>>();
let handle = spawn(move || server_loop::<G>(rx, state));
ServerRef { tx, pid: handle.pid() }
ServerBuilder::new(state).start()
}
/// Like [`start`], but spawns the server under an explicit supervisor pid (via
/// [`spawn_under`]) so it slots into the supervision tree.
pub fn start_under<G: GenServer>(supervisor: Pid, state: G) -> ServerRef<G> {
let (tx, rx) = channel::<Envelope<G>>();
let handle = spawn_under(supervisor, move || server_loop::<G>(rx, state));
ServerRef { tx, pid: handle.pid() }
ServerBuilder::new(state).under(supervisor).start()
}
fn server_loop<G: GenServer>(rx: Receiver<Envelope<G>>, state: G) {
fn server_loop<G: GenServer>(
rx: Receiver<Envelope<G>>,
state: G,
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);
@@ -199,22 +265,63 @@ fn server_loop<G: GenServer>(rx: Receiver<Envelope<G>>, state: G) {
}
}
let mut guard = Terminate(state);
guard.0.init();
loop {
match rx.recv() {
Ok(Envelope::Call(request, reply_tx)) => {
let reply = guard.0.handle_call(request);
fn dispatch<G: GenServer>(state: &mut G, env: Envelope<G>) {
match env {
Envelope::Call(request, reply_tx) => {
let reply = state.handle_call(request);
// The caller may have gone away (e.g. cancelled while parked);
// a failed reply send is not the server's problem.
let _ = reply_tx.send(reply);
}
Ok(Envelope::Cast(request)) => guard.0.handle_cast(request),
// All ServerRefs dropped → inbox closed → graceful shutdown.
Err(_) => break,
Envelope::Cast(request) => state.handle_cast(request),
}
// Observation point so a server whose inbox is never empty stays
}
let mut guard = Terminate(state);
guard.0.init();
loop {
if infos.is_empty() {
// Nothing to select over: park on the inbox alone, exactly the
// pre-v0.8 loop.
match rx.recv() {
Ok(env) => dispatch(&mut guard.0, env),
// All ServerRefs dropped → inbox closed → graceful shutdown.
Err(_) => break,
}
} else {
// Arm priority: infos (declaration order), then the inbox. The
// arm slice is rebuilt per iteration because `infos` shrinks as
// arms close.
let i = {
let mut arms: Vec<&dyn Selectable> = Vec::with_capacity(infos.len() + 1);
for r in &infos {
arms.push(r);
}
arms.push(&rx);
select(&arms)
};
if i < infos.len() {
match infos[i].try_recv() {
Ok(Some(info)) => guard.0.handle_info(info),
// Single-receiver: nothing can drain the arm between
// select's ready and our try_recv.
Ok(None) => debug_assert!(false, "ready info arm was empty"),
// Senders all gone: drop the arm, keep serving. `remove`
// (not swap_remove) — order is priority.
Err(_) => {
infos.remove(i);
}
}
} else {
match rx.try_recv() {
Ok(Some(env)) => dispatch(&mut guard.0, env),
Ok(None) => debug_assert!(false, "ready inbox was empty"),
Err(_) => break,
}
}
}
// Observation point so a server whose arms are never empty stays
// preemptible and cancellable.
crate::check!();
}
+1 -1
View File
@@ -46,7 +46,7 @@ static ALLOCATOR: preempt::PreemptingAllocator = preempt::PreemptingAllocator;
// ---------------------------------------------------------------------------
pub use channel::{channel, select, select_timeout, Receiver, RecvError, RecvTimeoutError, Selectable, Sender};
pub use gen_server::{CallError, CallTimeoutError, CastError, GenServer, ServerRef};
pub use gen_server::{CallError, CallTimeoutError, CastError, GenServer, ServerBuilder, ServerRef};
pub use link::{link, trap_exit, unlink, ExitSignal};
pub use monitor::{demonitor, monitor, Down, DownReason, Monitor, MonitorId};
pub use mutex::{LockTimeout, Mutex, MutexGuard};