Monitors are created at runtime (you watch a worker you just spawned in a handler), so down arms can't ride the static info list. init grows a &ServerCtx parameter (breaking; no-op default) whose clonable Watcher hands Monitors to the loop over a control arm; the loop selects the live down arms ahead of everything else. Arm priority: downs → control → infos → inbox. A delivered Down retires its arm (monitors are one-shot); a state that never clones the Watcher closes the control arm after init and the loop falls back to the plain-inbox park.
407 lines
16 KiB
Rust
407 lines
16 KiB
Rust
//! gen_server — synchronous call / asynchronous cast over a single actor.
|
|
//!
|
|
//! A thin request-reply layer on top of [`channel`](crate::channel), modelled
|
|
//! on Erlang's `gen_server`. A *server* is an actor owning a state value that
|
|
//! implements [`GenServer`]; clients hold a clonable [`ServerRef`] and issue
|
|
//! [`call`](ServerRef::call) (synchronous, returns a reply) or
|
|
//! [`cast`](ServerRef::cast) (fire-and-forget).
|
|
//!
|
|
//! ## One inbox, many arms
|
|
//!
|
|
//! 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
|
|
//!
|
|
//! Detection falls out of channel closure, so no monitor is required:
|
|
//! - if the server is already gone, its inbox is closed and the `send` in
|
|
//! `call`/`cast` fails → [`CallError::ServerDown`] / [`CastError::ServerDown`];
|
|
//! - if the server dies *after* a call is enqueued but before it replies
|
|
//! (a handler panic, or a cooperative `request_stop`), the reply sender is
|
|
//! dropped as the server's stack unwinds, closing the reply channel; the
|
|
//! parked caller wakes and its `recv` returns `Err` → `ServerDown`.
|
|
//!
|
|
//! ## Lifecycle / callbacks
|
|
//!
|
|
//! [`GenServer::init`] runs once inside the server actor before the first
|
|
//! message; [`GenServer::terminate`] runs on the way out. terminate is wired
|
|
//! through a drop guard, so it fires on *every* exit path — graceful inbox
|
|
//! close, a handler panic, or a cooperative `request_stop` — not only the clean
|
|
//! one. Keep it cheap and non-blocking: it may run mid-unwind, and a panic
|
|
//! inside it during an unwind aborts the process (a double panic).
|
|
//!
|
|
//! ## Not here (yet)
|
|
//!
|
|
//! 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, select, Receiver, Selectable, Sender};
|
|
use crate::monitor::{Down, Monitor};
|
|
use crate::pid::Pid;
|
|
use crate::scheduler::{spawn, spawn_under};
|
|
|
|
/// Behaviour for a gen_server: a state value plus call/cast handlers.
|
|
///
|
|
/// `handle_call` and `handle_cast` are required; [`init`](Self::init) and
|
|
/// [`terminate`](Self::terminate) are optional lifecycle hooks with no-op
|
|
/// defaults.
|
|
pub trait GenServer: Send + 'static {
|
|
/// Synchronous request type (carried by [`ServerRef::call`]).
|
|
type Call: Send + 'static;
|
|
/// Reply type returned for a `Call`.
|
|
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. The
|
|
/// [`ServerCtx`] is the loop's one runtime hook: clone its [`Watcher`]
|
|
/// into the state here to be able to [`watch`](Watcher::watch) monitors
|
|
/// from any later handler.
|
|
fn init(&mut self, _ctx: &ServerCtx) {}
|
|
|
|
/// Handle a synchronous call and produce the reply sent back to the caller.
|
|
fn handle_call(&mut self, request: Self::Call) -> Self::Reply;
|
|
|
|
/// 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) {}
|
|
|
|
/// Handle a [`Down`] from a monitor handed to the loop via
|
|
/// [`Watcher::watch`]. Default: drop it.
|
|
fn handle_down(&mut self, _down: Down) {}
|
|
|
|
/// Runs as the server actor exits, on any exit path (see module docs).
|
|
fn terminate(&mut self) {}
|
|
}
|
|
|
|
/// What travels the server's single inbox channel: a synchronous call (with a
|
|
/// reply sender) or an asynchronous cast.
|
|
enum Envelope<G: GenServer> {
|
|
Call(G::Call, Sender<G::Reply>),
|
|
Cast(G::Cast),
|
|
}
|
|
|
|
/// A clonable handle to a running server. Cloning yields another sender to the
|
|
/// same inbox; the server lives until the last `ServerRef` is dropped, at which
|
|
/// point its inbox closes and the loop exits normally.
|
|
pub struct ServerRef<G: GenServer> {
|
|
tx: Sender<Envelope<G>>,
|
|
pid: Pid,
|
|
}
|
|
|
|
impl<G: GenServer> Clone for ServerRef<G> {
|
|
fn clone(&self) -> Self {
|
|
ServerRef { tx: self.tx.clone(), pid: self.pid }
|
|
}
|
|
}
|
|
|
|
/// Returned by [`ServerRef::call`] when the server is no longer reachable.
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
pub enum CallError {
|
|
/// The server was already gone, or died before replying.
|
|
ServerDown,
|
|
}
|
|
|
|
/// Returned by [`ServerRef::call_timeout`].
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
pub enum CallTimeoutError {
|
|
/// The server was already gone, or died before replying.
|
|
ServerDown,
|
|
/// The deadline passed before a reply arrived. The request stays in the
|
|
/// server's inbox: it will still be *handled*, but the reply is discarded
|
|
/// (the abandoned reply channel's receiver is dropped, so the server's
|
|
/// reply send fails harmlessly). Erlang behaves the same way; design
|
|
/// idempotent calls accordingly.
|
|
Timeout,
|
|
}
|
|
|
|
/// Returned by [`ServerRef::cast`] when the server is no longer reachable.
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
pub enum CastError {
|
|
/// The server inbox was closed (the server is gone).
|
|
ServerDown,
|
|
}
|
|
|
|
impl<G: GenServer> ServerRef<G> {
|
|
/// The server actor's pid — usable with `monitor`, `request_stop`, `link`.
|
|
pub fn pid(&self) -> Pid {
|
|
self.pid
|
|
}
|
|
|
|
/// Synchronous request-reply. Blocks (parking the calling actor) until the
|
|
/// server replies, or returns [`CallError::ServerDown`] if the server is or
|
|
/// becomes unreachable before a reply arrives.
|
|
pub fn call(&self, request: G::Call) -> Result<G::Reply, CallError> {
|
|
let (reply_tx, reply_rx) = channel::<G::Reply>();
|
|
self.tx
|
|
.send(Envelope::Call(request, reply_tx))
|
|
.map_err(|_| CallError::ServerDown)?;
|
|
reply_rx.recv().map_err(|_| CallError::ServerDown)
|
|
}
|
|
|
|
/// Bounded synchronous request-reply: like [`call`](Self::call), but
|
|
/// gives up after `timeout`, returning [`CallTimeoutError::Timeout`].
|
|
///
|
|
/// The roadmap sketched this as monitor + wait-reply-or-Down + demonitor;
|
|
/// that machinery is unnecessary here because server death is already
|
|
/// observable on the reply channel itself — the reply sender is dropped
|
|
/// as the server's stack unwinds, closing the channel and waking the
|
|
/// parked caller (see the module docs). So a bounded call is exactly
|
|
/// [`Receiver::recv_timeout`] on the reply channel: `Ok` on a reply,
|
|
/// `Disconnected` → [`CallTimeoutError::ServerDown`], `Timeout` →
|
|
/// [`CallTimeoutError::Timeout`]. Nothing is registered, so nothing can
|
|
/// leak on the timeout path by construction.
|
|
pub fn call_timeout(
|
|
&self,
|
|
request: G::Call,
|
|
timeout: std::time::Duration,
|
|
) -> Result<G::Reply, CallTimeoutError> {
|
|
let (reply_tx, reply_rx) = channel::<G::Reply>();
|
|
self.tx
|
|
.send(Envelope::Call(request, reply_tx))
|
|
.map_err(|_| CallTimeoutError::ServerDown)?;
|
|
reply_rx.recv_timeout(timeout).map_err(|e| match e {
|
|
crate::channel::RecvTimeoutError::Disconnected => CallTimeoutError::ServerDown,
|
|
crate::channel::RecvTimeoutError::Timeout => CallTimeoutError::Timeout,
|
|
})
|
|
}
|
|
|
|
/// Fire-and-forget request. Returns once the message is enqueued; does not
|
|
/// wait for the server to handle it. [`CastError::ServerDown`] if the inbox
|
|
/// is already closed.
|
|
pub fn cast(&self, request: G::Cast) -> Result<(), CastError> {
|
|
self.tx
|
|
.send(Envelope::Cast(request))
|
|
.map_err(|_| CastError::ServerDown)
|
|
}
|
|
}
|
|
|
|
/// The server loop's runtime hook, passed to [`GenServer::init`]. Currently
|
|
/// carries only the [`Watcher`]; opaque so fields can grow without breaking.
|
|
pub struct ServerCtx {
|
|
watcher: Watcher,
|
|
}
|
|
|
|
impl ServerCtx {
|
|
/// A clonable handle to the loop's monitor intake. Store it in the state
|
|
/// during `init` to watch monitors from later handlers.
|
|
pub fn watcher(&self) -> Watcher {
|
|
self.watcher.clone()
|
|
}
|
|
|
|
/// Shorthand for `ctx.watcher().watch(m)` when watching during `init`.
|
|
pub fn watch(&self, m: Monitor) {
|
|
self.watcher.watch(m)
|
|
}
|
|
}
|
|
|
|
/// Hands [`Monitor`]s to a server loop; their [`Down`]s are dispatched to
|
|
/// [`GenServer::handle_down`]. Down arms outrank every other arm (a death
|
|
/// notice cannot be starved), and a delivered `Down` retires its arm —
|
|
/// monitors are one-shot.
|
|
#[derive(Clone)]
|
|
pub struct Watcher {
|
|
tx: Sender<Monitor>,
|
|
}
|
|
|
|
impl Watcher {
|
|
/// Transfer `m` to the server loop. If the server is already gone the
|
|
/// monitor is silently dropped (its queued `Down`, if any, with it) —
|
|
/// there is no loop left to care.
|
|
pub fn watch(&self, m: Monitor) {
|
|
let _ = self.tx.send(m);
|
|
}
|
|
}
|
|
|
|
/// 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`]. Shorthand for `ServerBuilder::new(state).start()`.
|
|
pub fn start<G: GenServer>(state: G) -> ServerRef<G> {
|
|
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> {
|
|
ServerBuilder::new(state).under(supervisor).start()
|
|
}
|
|
|
|
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);
|
|
impl<G: GenServer> Drop for Terminate<G> {
|
|
fn drop(&mut self) {
|
|
self.0.terminate();
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
Envelope::Cast(request) => state.handle_cast(request),
|
|
}
|
|
}
|
|
|
|
let mut guard = Terminate(state);
|
|
|
|
// The control arm: Watchers feed Monitors 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 (watch_tx, watch_rx) = channel::<Monitor>();
|
|
guard.0.init(&ServerCtx { watcher: Watcher { tx: watch_tx } });
|
|
|
|
let mut monitors: Vec<Monitor> = Vec::new();
|
|
let mut watch_open = true;
|
|
|
|
loop {
|
|
if monitors.is_empty() && !watch_open && 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: downs, then the control arm, then infos (each in
|
|
// declaration order), then the inbox. The arm slice is rebuilt
|
|
// per iteration because every set but the inbox shrinks or grows.
|
|
let nd = monitors.len();
|
|
let nw = watch_open as usize;
|
|
let i = {
|
|
let mut arms: Vec<&dyn Selectable> =
|
|
Vec::with_capacity(nd + nw + infos.len() + 1);
|
|
for m in &monitors {
|
|
arms.push(&m.rx);
|
|
}
|
|
if watch_open {
|
|
arms.push(&watch_rx);
|
|
}
|
|
for r in &infos {
|
|
arms.push(r);
|
|
}
|
|
arms.push(&rx);
|
|
select(&arms)
|
|
};
|
|
if i < nd {
|
|
// A Down retires its arm either way: delivered (one-shot) or
|
|
// closed without delivering (defensive; shouldn't happen).
|
|
let m = monitors.remove(i);
|
|
if let Ok(Some(down)) = m.rx.try_recv() {
|
|
guard.0.handle_down(down);
|
|
}
|
|
} else if i < nd + nw {
|
|
match watch_rx.try_recv() {
|
|
Ok(Some(m)) => monitors.push(m),
|
|
// Single-receiver: nothing can drain the arm between
|
|
// select's ready and our try_recv.
|
|
Ok(None) => debug_assert!(false, "ready control arm was empty"),
|
|
// Every Watcher gone: stop selecting on the arm.
|
|
Err(_) => watch_open = false,
|
|
}
|
|
} else if i < nd + nw + infos.len() {
|
|
let j = i - nd - nw;
|
|
match infos[j].try_recv() {
|
|
Ok(Some(info)) => guard.0.handle_info(info),
|
|
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(j);
|
|
}
|
|
}
|
|
} 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!();
|
|
}
|
|
}
|