feat(server): add Tokio UDP runtime #81
No files matched your search
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user