fix(server): keep shutdown responsive during sends

This commit is contained in:
Nils Andreas Svee committed 2026-08-13 01:50:25 +07:00
1 parent 85c432fcf8
commit 41e39bd5b9
2 files changed
+21 -13

No files matched your search

+8 -9
View File
@@ -202,15 +202,14 @@ Do not rewrite the protocol evidence to match them.
The only interoperability constraint is negative — expiry must never be The only interoperability constraint is negative — expiry must never be
signalled — and silence satisfies it. A zero idle timeout expires a session at signalled — and silence satisfies it. A zero idle timeout expires a session at
the next evaluation; it is not "never expire". the next evaluation; it is not "never expire".
- **Expiry is packet-driven until the runtime exists.** Logical expiry is - **Logical expiry is request-exact; physical reclamation also runs on the
timestamp-based, but reclamation happens in a global sweep run after runtime's fixed maintenance cadence.** Every authenticated, structurally valid
authentication and before dispatch, so an expired session may stay resident request runs the global sweep before dispatch, so a request at the deadline
until the next authenticated, structurally valid request. Nothing observable finds the session expired and a stale session cannot deny an open. The Tokio
turns on it: the table is already bounded, and the sweep runs before capacity runtime invokes that same core sweep once per second so dead sessions do not
is judged, so a stale session cannot deny an open. Because the sweep is global remain resident when there is no further traffic. This cadence is internal
it also runs for a no-test open, where upstream was observed not to reclaim. housekeeping, not protocol policy or public configuration, and the runtime
Do not add a public timer or tick abstraction for this; the Tokio slice may add must not inspect session internals or duplicate the expiry rule.
scheduled sweeps.
- **The maximum-duration deadline is `first served echo + maximum + 2 s`.** The - **The maximum-duration deadline is `first served echo + maximum + 2 s`.** The
origin is measured behavior: neither the open, nor a rejected first echo, nor a origin is measured behavior: neither the open, nor a rejected first echo, nor a
rate-limited one starts it. The two-second grace matches the measured upstream rate-limited one starts it. The two-second grace matches the measured upstream
+13 -4
View File
@@ -62,9 +62,10 @@ impl Server {
/// Serves datagrams until `shutdown` completes. /// Serves datagrams until `shutdown` completes.
/// ///
/// Graceful shutdown sends no session-close packets and leaves no hidden /// Graceful shutdown sends no session-close packets and leaves no hidden
/// receive task behind. Per-packet send failures and short sends drop only /// receive task behind, even while a reply send is waiting for socket
/// that reply; receive failures and internal core failures terminate the /// writability. Per-packet send failures and short sends drop only that
/// loop with an error. /// reply; receive failures and internal core failures terminate the loop
/// with an error.
pub async fn run<F>(&mut self, shutdown: F) -> Result<(), ServerRuntimeError> pub async fn run<F>(&mut self, shutdown: F) -> Result<(), ServerRuntimeError>
where where
F: Future<Output = ()>, F: Future<Output = ()>,
@@ -89,7 +90,15 @@ impl Server {
} }
if let Some(reply) = self.core.handle_datagram(peer, &self.recv_buffer[..len])? { if let Some(reply) = self.core.handle_datagram(peer, &self.recv_buffer[..len])? {
let sent = self.socket.send_to(&reply, peer).await; let send = self.socket.send_to(&reply, peer);
tokio::pin!(send);
let sent = loop {
tokio::select! {
_ = &mut shutdown => return Ok(()),
_ = maintenance.tick() => self.core.maintain(),
sent = &mut send => break sent,
}
};
if !matches!(sent, Ok(len) if len == reply.len()) { if !matches!(sent, Ok(len) if len == reply.len()) {
continue; continue;
} }