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:
@@ -135,3 +135,97 @@ fn call_after_server_gone_is_server_down() {
|
||||
});
|
||||
assert_eq!(*got.lock().unwrap(), Some(Err(CallError::ServerDown)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// call_timeout
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use smarm::gen_server::CallTimeoutError;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Replies after sleeping `delay_ms` (parking the server actor, not the OS
|
||||
/// thread), so callers can race a deadline against the reply.
|
||||
struct Slow;
|
||||
|
||||
impl GenServer for Slow {
|
||||
type Call = u64; // delay in ms
|
||||
type Reply = u64;
|
||||
type Cast = ();
|
||||
|
||||
fn handle_call(&mut self, delay_ms: u64) -> u64 {
|
||||
if delay_ms > 0 {
|
||||
smarm::sleep(Duration::from_millis(delay_ms));
|
||||
}
|
||||
delay_ms
|
||||
}
|
||||
|
||||
fn handle_cast(&mut self, _: ()) {}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_timeout_returns_reply_within_deadline() {
|
||||
run(|| {
|
||||
let srv = start(Slow);
|
||||
assert_eq!(srv.call_timeout(0, Duration::from_secs(10)), Ok(0));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_timeout_times_out_on_slow_handler() {
|
||||
run(|| {
|
||||
let srv = start(Slow);
|
||||
let start_t = Instant::now();
|
||||
let r = srv.call_timeout(500, Duration::from_millis(50));
|
||||
assert_eq!(r, Err(CallTimeoutError::Timeout));
|
||||
let elapsed = start_t.elapsed();
|
||||
// Gave up at the deadline, not at the reply.
|
||||
assert!(elapsed >= Duration::from_millis(50));
|
||||
assert!(elapsed < Duration::from_millis(500));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_survives_an_abandoned_call_and_late_reply_is_discarded() {
|
||||
run(|| {
|
||||
let srv = start(Slow);
|
||||
assert_eq!(
|
||||
srv.call_timeout(100, Duration::from_millis(20)),
|
||||
Err(CallTimeoutError::Timeout)
|
||||
);
|
||||
// The timed-out request is still handled; its reply send fails
|
||||
// harmlessly (receiver dropped). The server must keep serving, and
|
||||
// the late reply must not leak into THIS call's reply channel.
|
||||
assert_eq!(srv.call_timeout(0, Duration::from_secs(10)), Ok(0));
|
||||
// Plain unbounded call still fine too.
|
||||
assert_eq!(srv.call(0), Ok(0));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_timeout_to_dead_server_is_server_down_not_timeout() {
|
||||
struct Bomb;
|
||||
impl GenServer for Bomb {
|
||||
type Call = ();
|
||||
type Reply = ();
|
||||
type Cast = ();
|
||||
fn handle_call(&mut self, _: ()) {
|
||||
panic!("kaboom");
|
||||
}
|
||||
fn handle_cast(&mut self, _: ()) {}
|
||||
}
|
||||
|
||||
run(|| {
|
||||
let srv = start(Bomb);
|
||||
// Dies mid-call: reply channel closes -> ServerDown (even though the
|
||||
// generous deadline never fires).
|
||||
assert_eq!(
|
||||
srv.call_timeout((), Duration::from_secs(10)),
|
||||
Err(CallTimeoutError::ServerDown)
|
||||
);
|
||||
// Already gone: inbox send fails -> ServerDown.
|
||||
assert_eq!(
|
||||
srv.call_timeout((), Duration::from_secs(10)),
|
||||
Err(CallTimeoutError::ServerDown)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user