From deca1e03d17b0673740332c59c9b4530dce1013a Mon Sep 17 00:00:00 2001 From: oracola91 <118899664+oracola91@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:26:03 +0300 Subject: [PATCH] Control channel: wait after a failed accept instead of retrying at once The control channel's 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, as the single-instance listener already does. The loop goes through a small helper so the wait can be tested without running out of file descriptors. --- crates/ui-egui/src/control.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index a4ab35d969..f9ada57356 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -916,6 +916,18 @@ pub fn serve(client: ControlClient) -> std::io::Result { serve_with(client, LIMITS) } +/// 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. The single-instance listener waits the same way. +#[cfg(not(target_arch = "wasm32"))] +const ACCEPT_RETRY: std::time::Duration = std::time::Duration::from_millis(100); + +/// The connections `incoming` accepts, calling `wait` after each failed accept. +#[cfg(not(target_arch = "wasm32"))] +fn accepted(incoming: impl Iterator>, mut wait: impl FnMut()) -> impl Iterator { + incoming.filter_map(move |stream| stream.map_err(|_| wait()).ok()) +} + #[cfg(not(target_arch = "wasm32"))] fn serve_with(client: ControlClient, limits: Limits) -> std::io::Result { use std::io::{BufReader, Write}; @@ -926,7 +938,7 @@ fn serve_with(client: ControlClient, limits: Limits) -> std::io::Result std::io::Result { #[cfg(test)] mod transport_tests { + /// 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. + #[cfg(not(target_arch = "wasm32"))] + #[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"); + } + /// Before authenticating, a line that isn't JSON gets a parse error and closes the /// connection, as [`super::serve`] documents for any first request other than `auth`. #[cfg(not(target_arch = "wasm32"))]