feat(serve): graceful shutdown + connection registry (v0.2 chunk 2)
Shutdown is drain-then-force-stop: flag.store(true) -> sup_h.join() (the no-more-accepts barrier) -> cast BeginDrain -> poll ConnCount every 50ms until 0 or drain_timeout; past the deadline each tick casts ForceStopConns and re-sweeps, so conns that registered after the deadline are still caught. Listener shutdown is redesigned vs chunk 1: Restart::Permanent -> Transient, wait-failure return -> panic (Transient restarts it), and a shared Arc<AtomicBool> flag checked at loop top with a 250ms wait_readable_timeout tick instead of request_stop — no request_stop on the supervisor or listeners anywhere. Historical note: the redesign was originally forced by smarm's then-lossy stop against QUEUED actors (fixed upstream in 7bab4d2); it is kept because flag-based listener shutdown is simpler and stop-semantics-free. Conns self-register with the registry (ConnStarted from the conn actor, not a listener cast): per-sender FIFO ordering is the only thing that prevents ConnEnded overtaking ConnStarted; see conn_registry module docs. conn_actor: in the catch_unwind(pipeline.run) Err branch, smarm::preempt::check_cancelled() runs BEFORE composing the 500 — smarm's stop is an undowncastable panic_any(StopSentinel), so the catch_unwind would otherwise swallow a stop and 500-and-keep-running. The stop flag is persistent, so check_cancelled re-raises cleanly. Handle.shutdown() wakes the root via a 100ms try_recv poll (SHUTDOWN_POLL): a cross-thread Sender::send's unpark is a try_with_runtime no-op without runtime TLS on the sending thread (still true on smarm 8e5b754; cross-thread unpark is a recorded smarm roadmap candidate — this poll dies with it). Two smarm bugs were found during this chunk and fixed upstream: lossy stop against QUEUED actors (7bab4d2) and the terminal-wake shutdown stall/hang (eddf3fe); post-mortem in artefact smarm-bug-terminal-wake.md.
This commit is contained in:
+38
-1
@@ -14,10 +14,13 @@
|
||||
//! during those parks, other connection actors progress freely.
|
||||
|
||||
use crate::conn::{Body, Conn, RespBody};
|
||||
use crate::conn_registry::{Cast, ConnRegistry, DeregisterGuard};
|
||||
use crate::net::OwnedFd;
|
||||
use crate::parser::{self, ParseError};
|
||||
use crate::plug::Pipeline;
|
||||
|
||||
use smarm::ServerRef;
|
||||
|
||||
use std::io::{self, ErrorKind};
|
||||
use std::os::fd::RawFd;
|
||||
use std::time::Duration;
|
||||
@@ -57,14 +60,29 @@ impl Default for ConnLimits {
|
||||
// run_connection — entry point spawned by the listener actor.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn run_connection(fd: OwnedFd, pipeline: Pipeline, limits: ConnLimits) {
|
||||
pub fn run_connection(
|
||||
fd: OwnedFd,
|
||||
pipeline: Pipeline,
|
||||
limits: ConnLimits,
|
||||
registry: ServerRef<ConnRegistry>,
|
||||
) {
|
||||
// The OwnedFd cleans up via Drop on any exit path (panic, error, or
|
||||
// normal close). No explicit close calls below.
|
||||
let raw = fd.as_raw();
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(limits.initial_read_buf);
|
||||
|
||||
// Self-register (initially idle: no request head parsed yet) and arm
|
||||
// the deregistration guard. Both casts come from this actor, so
|
||||
// Started always precedes Ended in the registry's inbox — see
|
||||
// conn_registry module docs for why the listener must not do this.
|
||||
let me = smarm::self_pid();
|
||||
let _ = registry.cast(Cast::ConnStarted(me));
|
||||
let _guard = DeregisterGuard::new(registry.clone(), me, Cast::ConnEnded);
|
||||
|
||||
loop {
|
||||
// ----- 1. Read until we have a full request head. -----
|
||||
// We are idle until a head parses: stoppable by a draining
|
||||
// registry while parked here.
|
||||
let parsed = match read_head(raw, &mut buf, &limits) {
|
||||
Ok(p) => p,
|
||||
Err(ReadHeadErr::ClientClosed) => {
|
||||
@@ -80,6 +98,7 @@ pub fn run_connection(fd: OwnedFd, pipeline: Pipeline, limits: ConnLimits) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let _ = registry.cast(Cast::ConnBusy(me));
|
||||
|
||||
// ----- 2. Read body. -----
|
||||
let body_len = parsed.content_length.unwrap_or(0);
|
||||
@@ -117,6 +136,16 @@ pub fn run_connection(fd: OwnedFd, pipeline: Pipeline, limits: ConnLimits) {
|
||||
let mut response_conn = match result {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
// Distinguish a genuine handler panic from smarm's stop
|
||||
// sentinel, which is also a panic payload and which this
|
||||
// catch_unwind would otherwise swallow — turning a
|
||||
// graceful stop into a 500-and-keep-running. The stop
|
||||
// flag is persistent (not consumed by raising the
|
||||
// sentinel), so if we were stopped this re-raises it
|
||||
// here, outside the catch, and we unwind properly (the
|
||||
// fd and registry guards clean up).
|
||||
smarm::preempt::check_cancelled();
|
||||
|
||||
// Compose a 500 manually; the original Conn was moved into
|
||||
// the closure.
|
||||
let mut c = Conn::new();
|
||||
@@ -143,6 +172,14 @@ pub fn run_connection(fd: OwnedFd, pipeline: Pipeline, limits: ConnLimits) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Response is on the wire; nothing is owed. Going idle here makes
|
||||
// us stoppable by a draining registry while we park for the next
|
||||
// keep-alive request. (If the next request is already pipelined in
|
||||
// `buf`, the very next read_head parses it without parking and we
|
||||
// go Busy again — a draining registry's request_stop may still
|
||||
// catch us, which is acceptable: drain means no new work.)
|
||||
let _ = registry.cast(Cast::ConnIdle(me));
|
||||
|
||||
// Drop the request bytes (head + body) from `buf`; anything past
|
||||
// them is the start of the next pipelined request.
|
||||
let consumed = head_len + body_len;
|
||||
|
||||
Reference in New Issue
Block a user