fix(quic-interop): QlogWriter swallows post-close writes
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
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user