From 6cd25848b0872dcaa007e55418c4c6c7acea62a0 Mon Sep 17 00:00:00 2001 From: Sergei Minaev Date: Thu, 10 Sep 2026 13:17:37 +0100 Subject: [PATCH] test(qwp): close TestWebSocketServer connections gracefully so scripted drops deliver their acks ClientHandler.close() closed the socket outright. With the peer's trailing frames still unread, that close is a TCP RST, and the Nagle-held second ack write (STATUS_DURABLE_ACK) of the last acked batch was purged with it. The drainer then saw a reset with no ack progress, and BackgroundDrainerMidDrainCapabilityGapTest#testDeliveringBetweenTwoGapWindowsGrantsAFreshSettleBudget failed on the JDK 8 CI job (java-questdb-client #91, run 34368418967). Now: TCP_NODELAY on accepted sockets, shutdownOutput() before close() so the written frames are followed by FIN, a bounded drain of unread input when the close runs on the read thread (the scripted-drop path), and no self-join of the read thread -- that join(5000) inside the synchronized handler callback serialized every scripted drop at 5 s. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_017S9gyXv4MwoDHEohA9At5d --- .../qwp/websocket/TestWebSocketServer.java | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/core/src/test/java/io/questdb/client/test/cutlass/qwp/websocket/TestWebSocketServer.java b/core/src/test/java/io/questdb/client/test/cutlass/qwp/websocket/TestWebSocketServer.java index 908e3bd5..87576a24 100644 --- a/core/src/test/java/io/questdb/client/test/cutlass/qwp/websocket/TestWebSocketServer.java +++ b/core/src/test/java/io/questdb/client/test/cutlass/qwp/websocket/TestWebSocketServer.java @@ -398,6 +398,7 @@ public void start() throws IOException { while (running.get()) { try { Socket clientSocket = serverSocket.accept(); + clientSocket.setTcpNoDelay(true); ClientHandler clientHandler = new ClientHandler(clientSocket); clients.add(clientHandler); clientHandler.start(); @@ -445,12 +446,22 @@ public class ClientHandler implements Closeable { @Override public void close() { running.set(false); + try { + // FIN after the frames already written, never an RST: closing with unread input + // resets the connection and purges anything still queued for the peer. + socket.shutdownOutput(); + if (readThread == Thread.currentThread()) { + drainInput(); + } + } catch (IOException e) { + // ignore + } try { socket.close(); } catch (IOException e) { // ignore } - if (readThread != null) { + if (readThread != null && readThread != Thread.currentThread()) { try { readThread.join(5000); } catch (InterruptedException e) { @@ -459,6 +470,21 @@ public void close() { } } + private void drainInput() throws IOException { + socket.setSoTimeout(20); + byte[] scratch = new byte[8192]; + long deadlineNanos = System.nanoTime() + 200_000_000L; + while (System.nanoTime() < deadlineNanos) { + try { + if (in.read(scratch) < 0) { + return; + } + } catch (SocketTimeoutException e) { + return; + } + } + } + public synchronized void sendBinary(byte[] data) throws IOException { writeFrame(WebSocketOpcode.BINARY, data, data.length); }