From 2a08ad32443498bbee480044c19022eeceb329bc Mon Sep 17 00:00:00 2001 From: Mojtaba Jafarzadeh Date: Sat, 24 Oct 2026 22:35:05 +0330 Subject: [PATCH] collab-net: wait longer for one-shot server replies Publishing uploads a whole file, and a large project on a slow link takes the server several minutes to receive and answer. The one-shot request connections (hosting, the publish upload, a library, a plugin directory) waited the session idle timeout (45s), so a large publish timed out with "no answer from the server" before the server finished. Add a dedicated request_reply_timeout (5m) for these connections, keeping the session idle timeout unchanged. Co-Authored-By: Claude --- crates/collab-net/src/limits.rs | 20 ++++++++++++++++++++ crates/collab-net/src/manage.rs | 8 +++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/crates/collab-net/src/limits.rs b/crates/collab-net/src/limits.rs index a0647fa6..0321b36c 100644 --- a/crates/collab-net/src/limits.rs +++ b/crates/collab-net/src/limits.rs @@ -24,6 +24,12 @@ pub struct NetLimits { pub hello_timeout: Duration, /// A connection with no traffic for this long is dead. pub idle_timeout: Duration, + /// How long a one-shot request connection (hosting, a library, a plugin directory — and, most + /// importantly, publishing a file) waits for the server's reply before giving up. Publishing + /// uploads a whole file and a large one on a slow link takes the server a while to receive and + /// answer, so this is longer than `idle_timeout` (a large project on a slow connection can + /// easily need several minutes). + pub request_reply_timeout: Duration, /// A quiet client pings this often (keeps it under `idle_timeout`). pub keepalive: Duration, /// Messages queued for one client before it counts as stuck and is disconnected. @@ -68,6 +74,7 @@ impl Default for NetLimits { burst: 400.0, hello_timeout: Duration::from_secs(10), idle_timeout: Duration::from_secs(45), + request_reply_timeout: Duration::from_secs(300), keepalive: Duration::from_secs(15), queue: 1024, write_timeout: Duration::from_secs(10), @@ -85,3 +92,16 @@ impl Default for NetLimits { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn request_reply_wait_is_longer_than_idle() { + let d = NetLimits::default(); + // Publishing uploads a whole file; a slow link must not cut it off at the session idle + // timeout. Keep the reply wait comfortably above it. + assert!(d.request_reply_timeout > d.idle_timeout); + } +} diff --git a/crates/collab-net/src/manage.rs b/crates/collab-net/src/manage.rs index 6e995bdb..843f91cf 100644 --- a/crates/collab-net/src/manage.rs +++ b/crates/collab-net/src/manage.rs @@ -196,13 +196,15 @@ pub fn plugins(server: &Endpoint, key: &str, req: PluginReq, limits: &NetLimits, Ok(r) } -/// A WebSocket to `server` that waits for answers up to the idle timeout (a large upload takes the -/// server a while to keep). +/// A WebSocket to `server` that waits for answers up to the request-reply timeout (a large upload +/// takes the server a while to keep). fn connect(server: &Endpoint, limits: &NetLimits, timeout: Duration) -> Result { let (stream, _, _) = open(server, timeout, limits)?; let (ws, _) = tungstenite::client::client_with_config(server.url(), stream, Some(config(limits.max_server_message))) .map_err(|e| format!("{} doesn't answer as a FigCraft server: {e}", server.display()))?; - ws.get_ref().set_read_timeout(Some(limits.idle_timeout)).map_err(|e| e.to_string())?; + // A publish uploads a whole file; a large project on a slow link can take several minutes to + // receive and answer. The wait uses `request_reply_timeout`, not the session `idle_timeout`. + ws.get_ref().set_read_timeout(Some(limits.request_reply_timeout)).map_err(|e| e.to_string())?; Ok(ws) }