From 2071ff599d99912b42db5ff99afffec7ed3fff35 Mon Sep 17 00:00:00 2001 From: oracola91 <118899664+oracola91@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:49:51 +0300 Subject: [PATCH] Control server: wait after a failed accept instead of retrying at once The accepting thread dropped failed accepts with incoming().flatten() and asked again at once. When accepting keeps failing, for example while the process has no file descriptors left, that loop keeps a core busy until it recovers. Wait 100 ms after a failed accept. The loop goes through a small helper so the wait can be tested without running out of file descriptors. --- apps/deckcraft/src/control_server.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/deckcraft/src/control_server.rs b/apps/deckcraft/src/control_server.rs index b4b80c1d..f3d963d0 100644 --- a/apps/deckcraft/src/control_server.rs +++ b/apps/deckcraft/src/control_server.rs @@ -61,12 +61,22 @@ pub fn start(port: u16, ctx: egui::Context) -> Receiver { } } +/// How long the accepting thread waits after a failed accept. Accepting can keep failing, for +/// example while the process has no file descriptors left, and retrying at once would keep a +/// core busy until it recovers. +const ACCEPT_RETRY: Duration = Duration::from_millis(100); + +/// The connections `incoming` accepts, calling `wait` after each failed accept. +fn accepted(incoming: impl Iterator>, mut wait: impl FnMut()) -> impl Iterator { + incoming.filter_map(move |stream| stream.map_err(|_| wait()).ok()) +} + /// Accept connections on `listener` under `limits`; requests arrive on the returned receiver. fn serve_listener(listener: TcpListener, limits: Limits, ctx: egui::Context) -> Receiver { let (tx, rx) = sync_channel::(limits.queue.max(1)); let active = Arc::new(AtomicUsize::new(0)); std::thread::spawn(move || { - for mut stream in listener.incoming().flatten() { + for mut stream in accepted(listener.incoming(), || std::thread::sleep(ACCEPT_RETRY)) { let _ = stream.set_write_timeout(Some(limits.write_timeout)); if active.fetch_add(1, Ordering::SeqCst) >= limits.max_clients { active.fetch_sub(1, Ordering::SeqCst); @@ -234,6 +244,18 @@ fn serve(stream: TcpStream, tx: &SyncSender, ctx: &egui::Context #[cfg(test)] mod tests { use super::*; + + /// A failed accept is followed by a wait before the next one, so a listener that keeps + /// failing does not spin, and the connections around the failures still come through. + #[test] + fn a_failed_accept_waits_before_the_next() { + let failed = || Err(std::io::Error::other("no file descriptors left")); + let incoming = vec![failed(), Ok(1), failed(), failed(), Ok(2)]; + let mut waits = 0; + let connections: Vec = super::accepted(incoming.into_iter(), || waits += 1).collect(); + assert_eq!(connections, [1, 2]); + assert_eq!(waits, 3, "one wait per failed accept"); + } use std::net::SocketAddr; use std::time::Instant;