Skip to content

Commit 6c6dc40

Browse files
glasstigerclaude
andcommitted
Latch a terminal on sent-dict mirror overflow
ensureSentDictCapacity threw a bare LineSenderException when the lifetime-monotonic sent-dictionary mirror would exceed MAX_SENT_DICT_BYTES. accumulateSentDict runs after the frame's sendBinary, so that throw unwound to ioLoop -> fail() -> connectLoop with running still true, which reconnected and replayed the same frame; the mirror never shrinks, so the replay re-overflowed and the loop reconnected again -- an unbounded livelock instead of the loud failure the ceiling promises. recordFatal now flips running=false before the throw, so connectLoop's !running guard winds the loop down and checkError() surfaces the terminal to the producer. This matches sendCatchUpChunk's guard for the same ceiling. The throw stays, to unwind past the pending copyMemory. The path needs ~100M+ distinct symbols on one connection to reach, so it is defensive; the producer's GlobalSymbolDictionary would OOM first. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 94e3b1d commit 6c6dc40

1 file changed

Lines changed: 12 additions & 1 deletion

File tree

core/src/main/java/io/questdb/client/cutlass/qwp/client/sf/cursor/CursorWebSocketSendLoop.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2137,8 +2137,19 @@ private void ensureSentDictCapacity(long required) {
21372137
return;
21382138
}
21392139
if (required > MAX_SENT_DICT_BYTES) {
2140-
throw new LineSenderException("symbol dictionary mirror exceeds the maximum size ["
2140+
// Latch a terminal, do NOT just throw: accumulateSentDict runs AFTER
2141+
// the frame's sendBinary, so a bare throw unwinds to ioLoop -> fail()
2142+
// -> connectLoop, which (running still true) reconnects and replays the
2143+
// same frame, which re-overflows the never-shrinking mirror -- an
2144+
// unbounded reconnect livelock rather than the "fails loudly" the
2145+
// MAX_SENT_DICT_BYTES ceiling promises. recordFatal flips running=false
2146+
// so connectLoop's !running guard winds the loop down and checkError()
2147+
// surfaces the terminal, matching sendCatchUpChunk's guard for the same
2148+
// ceiling. The throw still unwinds past the pending copyMemory.
2149+
LineSenderException err = new LineSenderException("symbol dictionary mirror exceeds the maximum size ["
21412150
+ "required=" + required + ", max=" + MAX_SENT_DICT_BYTES + ']');
2151+
recordFatal(err);
2152+
throw err;
21422153
}
21432154
// Grow in long to avoid the capacity*2 int overflow (negative) that would
21442155
// otherwise degrade the doubling near 1 GB; clamp to the int ceiling.

0 commit comments

Comments
 (0)