Skip to content

Commit a233fcd

Browse files
futurepaulclaude
andcommitted
devstack front door: a client that goes closes the runtime's connection whole
A client's end done, the front door only half-closed the runtime's. workerd keeps a WebSocket connection whose client closed (half-closed, or with a Close frame), so each such pass waited on the runtime for good: a thread and two sockets per closed socket, the shell's own watch among them. A repro of reloads, each closing two WebSockets, held 28,207 threads and 56,413 fds in ten minutes, and then connections were reset (module loads failed ERR_CONNECTION_RESET). Now, the client gone, the runtime's connection is closed whole, as a proxy does once its client goes. Test: a client that goes, from a runtime that keeps a 101'd connection open and silent after it: the pass ends (before: it never did). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 1dbff0a commit a233fcd

1 file changed

Lines changed: 53 additions & 2 deletions

File tree

‎crates/devstack/src/front.rs‎

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,17 @@ fn pass(client: TcpStream, heard: &Heard) {
182182
// small writes (a socket's frames, a response's head) go at once
183183
let _ = client.set_nodelay(true);
184184
let _ = runtime.set_nodelay(true);
185-
let (Ok(client_in), Ok(runtime_in)) = (client.try_clone(), runtime.try_clone()) else { return };
186-
let up = thread::Builder::new().name("front-up".into()).spawn(move || copy(client_in, runtime));
185+
let (Ok(client_in), Ok(runtime_in), Ok(runtime_end)) = (client.try_clone(), runtime.try_clone(), runtime.try_clone()) else { return };
186+
// the client's end done (closed, or failed), the runtime's is closed
187+
// whole, as a proxy does once its client goes: workerd keeps a
188+
// connection it is only half-closed on, and so would this thread
189+
// waiting on it (a reload's dropped streams and sockets, by the
190+
// thousand). A client that half-closes and waits for its answer gets
191+
// none; no client here does.
192+
let up = thread::Builder::new().name("front-up".into()).spawn(move || {
193+
copy(client_in, runtime);
194+
let _ = runtime_end.shutdown(Shutdown::Both);
195+
});
187196
copy(runtime_in, client);
188197
if let Ok(up) = up {
189198
let _ = up.join();
@@ -201,6 +210,7 @@ fn copy(mut from: TcpStream, mut to: TcpStream) {
201210
#[cfg(test)]
202211
mod tests {
203212
use super::*;
213+
use std::io::Read;
204214

205215
const NAMED: &str = r#"[InspectorProxyWorker] handleProxyControllerIncomingMessage {"type":"reloadComplete","proxyData":{"userWorkerUrl":{"protocol":"http:","hostname":"127.0.0.1","port":"43765"},"userWorkerInspectorUrl":{"protocol":"ws:","hostname":"127.0.0.1","port":"43153","pathname":"/core:user:fragment"},"headers":{"MF-Proxy-Shared-Secret":"05346fd5"},"liveReload":false}}"#;
206216

@@ -236,4 +246,45 @@ mod tests {
236246
heard.ended();
237247
assert_eq!(heard.running(None, Duration::from_secs(5)), None, "gone: no wait");
238248
}
249+
250+
/// A client that goes, from a runtime that keeps the connection open and
251+
/// silent after it (workerd does so for a WebSocket whose client closed,
252+
/// half-closed or not): the pass ends, and lets the connection go. Before,
253+
/// each such pass waited on the runtime for good, a thread and two sockets
254+
/// a closed socket: a repro of reloads, each closing two WebSockets, held
255+
/// 28,000 in ten minutes, until connections were reset.
256+
#[test]
257+
fn a_client_that_goes_ends_its_pass_though_the_runtime_stays() {
258+
use std::sync::mpsc;
259+
let runtime = TcpListener::bind(("127.0.0.1", 0)).unwrap();
260+
let heard = Heard::default();
261+
heard.line(NAMED.replace("43765", &runtime.local_addr().unwrap().port().to_string()).as_bytes());
262+
let (stay, done) = mpsc::channel::<()>();
263+
thread::spawn(move || {
264+
let (mut s, _) = runtime.accept().unwrap();
265+
let mut buf = [0; 1024];
266+
let n = s.read(&mut buf).unwrap();
267+
assert!(buf[..n].ends_with(b"\r\n\r\n"), "a request");
268+
s.write_all(b"HTTP/1.1 101 Switching Protocols\r\nupgrade: websocket\r\nconnection: Upgrade\r\n\r\n").unwrap();
269+
// the client's end seen, the connection kept, and nothing said
270+
while s.read(&mut buf).is_ok_and(|n| n > 0) {}
271+
let _ = done.recv();
272+
});
273+
let front = TcpListener::bind(("127.0.0.1", 0)).unwrap();
274+
let at = front.local_addr().unwrap();
275+
let (passed_tx, passed) = mpsc::channel();
276+
thread::spawn(move || {
277+
pass(front.accept().unwrap().0, &heard);
278+
passed_tx.send(()).unwrap();
279+
});
280+
let mut client = TcpStream::connect(at).unwrap();
281+
client.write_all(b"GET /ws HTTP/1.1\r\nhost: x\r\nupgrade: websocket\r\nconnection: Upgrade\r\n\r\n").unwrap();
282+
let mut answer = [0; 12];
283+
client.read_exact(&mut answer).unwrap();
284+
assert_eq!(&answer, b"HTTP/1.1 101");
285+
drop(client);
286+
// bounded only so a failure says so rather than hanging the suite
287+
assert_eq!(passed.recv_timeout(Duration::from_secs(10)), Ok(()), "the pass ended");
288+
drop(stay);
289+
}
239290
}

0 commit comments

Comments
 (0)