feat(serve): supervised listener pool (v0.2 chunk 1)
OneForOne supervisor on the root actor, Restart::Permanent per listener. ChildSpec factory owns its fd via Arc<OwnedFd> (OwnedFd gains Sync); restarts reuse the fd — no re-dup, no fd-less window. wait_readable failure now exits-and-restarts instead of being fatal. Conn actors stay unsupervised bare spawns. Test hook INJECT_LISTENER_PANICS panics before accept so a pending connection must survive into the restarted listener. Roadmap: chunk 1 marked done; chunk 3 fork resolved to smarm-native timed waits (RFC 008 landed at 393cdd0, reaper option removed); chunk 2 drain-then-stop decided; v0.4-3(b) unblocked.
This commit is contained in:
@@ -225,3 +225,62 @@ fn middleware_can_short_circuit() {
|
||||
assert_eq!(http_status(&resp), 200);
|
||||
assert_eq!(http_body(&resp), b"ok");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Supervision (v0.2 chunk 1)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A panicking listener must be restarted by the pool supervisor, and the
|
||||
/// connection that was pending when it died must still be served.
|
||||
///
|
||||
/// Pool size is 1 on purpose: with a sibling listener the test would pass
|
||||
/// even if restarts were broken (the sibling would pick up the accept).
|
||||
/// With one listener, the request after the injected panic can only
|
||||
/// succeed if a fresh listener came up on the same fd.
|
||||
#[test]
|
||||
fn panicking_listener_restarts() {
|
||||
let pipe = Pipeline::new().plug(
|
||||
Router::new().get("/ping", |c: Conn, _n: Next| c.put_status(200).put_body("pong"))
|
||||
);
|
||||
|
||||
let port = free_port();
|
||||
let addr: SocketAddr = format!("127.0.0.1:{port}").parse().unwrap();
|
||||
std::thread::spawn(move || {
|
||||
let cfg = Config {
|
||||
listener_pool: 1,
|
||||
scheduler_threads: Some(2),
|
||||
..Config::new(addr)
|
||||
};
|
||||
serve_with(cfg, pipe).unwrap();
|
||||
});
|
||||
for _ in 0..50 {
|
||||
if TcpStream::connect(addr).is_ok() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
|
||||
// Sanity: server answers before the fault.
|
||||
let resp = send_request(port, b"GET /ping HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n");
|
||||
assert_eq!(http_status(&resp), 200);
|
||||
|
||||
// Arm the fault: the listener's next accept-loop iteration panics
|
||||
// *before* accepting, so our connection waits in the kernel backlog
|
||||
// until the restarted listener picks it up.
|
||||
urus::serve::INJECT_LISTENER_PANICS.store(1, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
let resp = send_request(port, b"GET /ping HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n");
|
||||
assert_eq!(http_status(&resp), 200, "request pending across the panic was not served");
|
||||
assert_eq!(http_body(&resp), b"pong");
|
||||
assert_eq!(
|
||||
urus::serve::INJECT_LISTENER_PANICS.load(std::sync::atomic::Ordering::Relaxed),
|
||||
0,
|
||||
"fault was never consumed — listener didn't wake for the connection"
|
||||
);
|
||||
|
||||
// The restarted listener keeps serving.
|
||||
for _ in 0..5 {
|
||||
let resp = send_request(port, b"GET /ping HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n");
|
||||
assert_eq!(http_status(&resp), 200);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user