feat(server): add Tokio UDP runtime #81

Merged
Lochnair merged 4 commits from feat/server-tokio-runtime into main 2026-08-13 04:35:06 +00:00
2 changed files with 21 additions and 13 deletions
Showing only changes of commit 41e39bd5b9 - Show all commits

No files matched your search

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