feat(gen_server): call_timeout via recv_timeout on the reply channel
Deviates deliberately from the roadmap's monitor-based sketch (monitor + wait reply-or-Down-or-deadline + demonitor): server death is already observable on the reply channel itself - the reply sender drops as the server unwinds, closing the channel and waking the parked caller. So a bounded call is exactly recv_timeout on the reply channel, mapping Disconnected -> ServerDown and Timeout -> Timeout. No registration exists, so a timed-out call leaks nothing by construction - the guarantee the monitor design had to engineer. Semantics on timeout match Erlang: the request stays in the inbox and is still handled; the late reply's send fails harmlessly against the dropped reply receiver. CallTimeoutError keeps ServerDown and Timeout distinguishable; the infallible call() is unchanged. Tests: reply within deadline, timeout against a slow (parking) handler with elapsed bounds, server survival across an abandoned call (late reply discarded, bounded and unbounded calls keep working), and ServerDown-not-Timeout for both death paths (mid-call panic and already-gone inbox).
This commit is contained in:
+43
-5
@@ -36,11 +36,9 @@
|
||||
//!
|
||||
//! ## Not here (yet)
|
||||
//!
|
||||
//! No `handle_info` and no call timeout. Both need machinery smarm currently
|
||||
//! defers: a call timeout needs a per-`recv` deadline (cf. the deferred
|
||||
//! `Signal::Timeout`), and `handle_info` needs the cross-channel mailbox merge
|
||||
//! that selective receive deliberately left unmade. They are slated to land
|
||||
//! together with timeouts.
|
||||
//! 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`.)
|
||||
|
||||
use crate::channel::{channel, Receiver, Sender};
|
||||
use crate::pid::Pid;
|
||||
@@ -100,6 +98,19 @@ pub enum CallError {
|
||||
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 {
|
||||
@@ -124,6 +135,33 @@ impl<G: GenServer> ServerRef<G> {
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user