From bd9d717dffd21ed8b55495489c40cd944a7113b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 00:40:21 +0000 Subject: [PATCH] fix(quic-interop): QlogWriter swallows post-close writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aioquic full-matrix run came back 0/7 with stack traces: java.io.IOException: Stream closed at QlogWriter.writeLineLocked(QlogWriter.kt:289) at QlogWriter.onPacketSent(QlogWriter.kt:126) at QuicConnectionWriter.emitQlogSent(...) The send loop runs concurrently with InteropClient's teardown sequence: 1. driver.close() — launches CLOSING → CLOSED async 2. qlogWriter?.close() — closes the file 3. delay(50) 4. scope.cancel() — kills coroutines Between (2) and (4), the send loop is still alive and emits packet_sent for the CONNECTION_CLOSE Initial. QlogWriter.writeLineLocked threw IOException into the send loop, propagated up, took down the connection — including connections that had completed transfer successfully. Fix: QlogWriter swallows post-close IOExceptions silently. An observer must NOT break the connection it's observing — that's the whole NoOp contract. Adds @Volatile closed flag, latches it both on explicit close() and on the first IOException; subsequent emits short-circuit. picoquic was 5/7 (predictable: M + S still open) so the regression was specifically aioquic-timing-sensitive. With this fix aioquic should return to 5/7 too. https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT --- .../quic/interop/runner/QlogWriter.kt | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/quic/interop/src/main/kotlin/com/vitorpamplona/quic/interop/runner/QlogWriter.kt b/quic/interop/src/main/kotlin/com/vitorpamplona/quic/interop/runner/QlogWriter.kt index 7f495f965..e334fa089 100644 --- a/quic/interop/src/main/kotlin/com/vitorpamplona/quic/interop/runner/QlogWriter.kt +++ b/quic/interop/src/main/kotlin/com/vitorpamplona/quic/interop/runner/QlogWriter.kt @@ -60,6 +60,12 @@ class QlogWriter( // append=false → truncate any prior trace at this path so a reused // QLOGDIR doesn't accumulate stale events from a previous run. private val writer: BufferedWriter = BufferedWriter(FileWriter(file, false)) + + /** Latched true once close() runs OR once a write throws IOException. + * Subsequent emits short-circuit instead of throwing into the + * send loop and tearing down an otherwise-healthy connection. */ + @Volatile + private var closed: Boolean = false private val lock = ReentrantLock() private val startMillis: Long = nowMillis() @@ -259,6 +265,7 @@ class QlogWriter( override fun close() { lock.withLock { + closed = true try { writer.flush() } finally { @@ -286,11 +293,22 @@ class QlogWriter( private fun writeLineLocked(line: String) { lock.withLock { - writer.write(line) - writer.write("\n") - // Flush every line so a hard-killed process still leaves a - // partial-but-parseable trace. - writer.flush() + // The send loop may still emit packet_sent events for the + // CONNECTION_CLOSE packet after the application calls close() + // — observers MUST NOT break the connection. Silently swallow + // post-close IOExceptions; the trace is what we have. + if (closed) return + try { + writer.write(line) + writer.write("\n") + // Flush every line so a hard-killed process still leaves a + // partial-but-parseable trace. + writer.flush() + } catch (_: java.io.IOException) { + // Stream closed under us. Latch closed so subsequent emits + // skip the lock and return immediately. + closed = true + } } }