Skip to content

Commit 034d8f3

Browse files
glasstigerclaude
andcommitted
Accumulate the tail of a partial-overlap delta into the mirror
accumulateSentDict dropped a frame entirely whenever deltaStart != sentDictCount. A delta that overlaps the mirror tip and extends past it (deltaStart < sentDictCount < deltaStart+deltaCount) was therefore discarded whole -- the new tail symbols never entered the mirror, which would leave a later reconnect catch-up incomplete and shift server-side ids. The producer only ever emits strictly contiguous, non-overlapping deltas, so this is currently unreachable, but it is load-bearing correctness resting on an invariant enforced elsewhere. Handle the overlap: skip the already-held prefix [deltaStart, sentDictCount) and copy only the new tail [sentDictCount, deltaStart+deltaCount). The steady-state case (deltaStart == sentDictCount) has skip == 0, so it is unchanged and free. A gap (deltaStart > sentDictCount, which the torn-dictionary guard rejects before send) now bails explicitly rather than implicitly. Also document that the I/O-thread mirror is a second, native copy of the dictionary (the producer's GlobalSymbolDictionary already holds the same symbols as Java Strings) -- so a memory-mode connection's steady-state dictionary footprint is ~2x the symbol set, an intentional cost of the reconnect-catch-up capability. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 27aad9e commit 034d8f3

1 file changed

Lines changed: 37 additions & 7 deletions

File tree

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

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,13 @@ public final class CursorWebSocketSendLoop implements QuietCloseable {
193193
// on reconnect it can re-register the whole dictionary on the fresh server
194194
// (which discards its dictionary on every disconnect) before replaying frames
195195
// whose deltas start above id 0. All of this is touched only by the I/O thread.
196+
// Footprint note: this mirror is a SECOND copy of the dictionary -- the same
197+
// symbols the producer's GlobalSymbolDictionary already holds as Java Strings --
198+
// kept as native UTF-8 bytes for the reconnect-catch-up capability. So a
199+
// memory-mode connection's steady-state dictionary footprint is ~2x the symbol
200+
// set. It is bounded by distinct-symbol count (not per-row) and never trimmed
201+
// for the connection's lifetime (a reconnect may need the whole dictionary at
202+
// any moment), so it cannot be dropped; it is an intentional cost of the feature.
196203
private final boolean deltaDictEnabled;
197204
private long sentDictBytesAddr;
198205
private int sentDictBytesCapacity;
@@ -2025,15 +2032,38 @@ private void accumulateSentDict(long payloadAddr, int payloadLen, int deltaStart
20252032
long p = payloadAddr + QwpConstants.HEADER_SIZE + NativeBufferWriter.varintSize(deltaStart);
20262033
long deltaCount = readVarintAt(p, limit);
20272034
p = varintEnd;
2028-
// Only a delta that extends exactly from our current tip introduces new
2029-
// symbols. deltaStart < sentDictCount is a replay/overlap we already hold;
2030-
// deltaStart > sentDictCount is rejected before send by the torn-dictionary
2031-
// guard in trySendOne, so it never reaches here.
2032-
if (deltaCount <= 0 || deltaStart != sentDictCount) {
2035+
// The mirror holds ids [0, sentDictCount). Accumulate ONLY the part of this
2036+
// frame's delta [deltaStart, deltaStart+deltaCount) that extends past the
2037+
// tip -- ids [sentDictCount, deltaStart+deltaCount). Cases:
2038+
// - deltaStart > sentDictCount: a gap. trySendOne's torn-dictionary guard
2039+
// rejects it before send, so it never reaches here; bail defensively
2040+
// rather than accumulate past a hole.
2041+
// - deltaEnd <= sentDictCount: a pure replay/overlap we already hold --
2042+
// nothing new.
2043+
// - deltaStart <= sentDictCount < deltaEnd: extend the mirror by the tail.
2044+
// deltaStart == sentDictCount is the steady-state case (skip == 0).
2045+
// Handling the partial overlap explicitly -- rather than dropping the whole
2046+
// frame whenever deltaStart != sentDictCount -- keeps the mirror complete
2047+
// even if a future producer ever emits a delta that overlaps the tip;
2048+
// silently dropping the new tail would leave the reconnect catch-up
2049+
// incomplete and shift server-side ids.
2050+
long deltaEnd = deltaStart + deltaCount;
2051+
if (deltaCount <= 0 || deltaStart > sentDictCount || deltaEnd <= sentDictCount) {
20332052
return;
20342053
}
2054+
// Walk past the already-held prefix [deltaStart, sentDictCount), then copy
2055+
// the new tail [sentDictCount, deltaEnd).
2056+
int skip = sentDictCount - deltaStart;
2057+
for (int i = 0; i < skip; i++) {
2058+
long len = readVarintAt(p, limit);
2059+
p = varintEnd + len;
2060+
if (p > limit) {
2061+
return; // malformed -- bail rather than corrupt the mirror
2062+
}
2063+
}
20352064
long regionStart = p;
2036-
for (long i = 0; i < deltaCount; i++) {
2065+
long newCount = deltaEnd - sentDictCount;
2066+
for (long i = 0; i < newCount; i++) {
20372067
long len = readVarintAt(p, limit);
20382068
p = varintEnd + len;
20392069
if (p > limit) {
@@ -2050,7 +2080,7 @@ private void accumulateSentDict(long payloadAddr, int payloadLen, int deltaStart
20502080
ensureSentDictCapacity((long) sentDictBytesLen + regionBytes);
20512081
Unsafe.getUnsafe().copyMemory(regionStart, sentDictBytesAddr + sentDictBytesLen, regionBytes);
20522082
sentDictBytesLen += regionBytes;
2053-
sentDictCount += (int) deltaCount;
2083+
sentDictCount += (int) newCount;
20542084
}
20552085

20562086
private void ensureSentDictCapacity(long required) {

0 commit comments

Comments
 (0)