Skip to content

Commit fef7203

Browse files
glasstigerclaude
andcommitted
Plug ensureConnected leak; consolidate varint helpers
Review cleanups: - ensureConnected's start()-failure catch now closes cursorSendLoop before nulling client. Previously it closed only the client; because the connected flag is set past the catch, a caller that retries -- re-entering ensureConnected and reassigning cursorSendLoop -- orphaned a recovered slot's ctor-seeded native mirror. That is a reachable leak on the recovered-slot + start()/dispatcher-OOM + retry path. close() is idempotent and frees the mirror via its loopNeverRan path, and also closes the shared client, so the following client.close() is a no-op. - Consolidated the triplicated raw-address varint writer into NativeBufferWriter.writeVarint (writeVarintDirect now delegates to it); PersistedSymbolDict and the catch-up frame builder in CursorWebSocketSendLoop use it, and PersistedSymbolDict.varintSize now reuses NativeBufferWriter.varintSize. - Alphabetized CursorSendEngine (getPersistedSymbolDict before getTotalBackpressureStalls before isDeltaDictEnabled) and moved PersistedSymbolDict.decodeVarint to the front of its private-static cluster. - readVarintAt gained the shift > 35 defensive bound that decodeVarint already has (unreachable today, all inputs are freshly-encoded or validated varints, but consistent). - CursorWebSocketSendLoopCatchUpAlignmentTest now uses the existing TestUtils.createTmpDir/removeTmpDir instead of reinventing them. Not changed: the reported dead CursorSendEngine.isMemoryMode() and a PersistedSymbolDict MAX_ENTRY_LEN constant do not exist at HEAD (the former removed earlier on this branch, the latter never present), and the NativeBufferWriter import is already correctly ordered. The buildAck/waitFor/rmDir/readVarint test helpers are duplicated across ~19 pre-existing test files, so consolidating them belongs in a dedicated repo-wide test-hygiene change rather than this feature PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 186ae10 commit fef7203

6 files changed

Lines changed: 77 additions & 93 deletions

File tree

core/src/main/java/io/questdb/client/cutlass/qwp/client/NativeBufferWriter.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,21 @@ public static int varintSize(long value) {
7676
return (64 - Long.numberOfLeadingZeros(value) + 6) / 7;
7777
}
7878

79+
/**
80+
* Writes {@code value} as an unsigned LEB128 varint directly at native address
81+
* {@code addr} and returns the address just past the last byte. The canonical
82+
* raw-address varint writer shared by the SF cursor's persisted dictionary and
83+
* catch-up frame builder.
84+
*/
85+
public static long writeVarint(long addr, long value) {
86+
while (value > 0x7F) {
87+
Unsafe.getUnsafe().putByte(addr++, (byte) ((value & 0x7F) | 0x80));
88+
value >>>= 7;
89+
}
90+
Unsafe.getUnsafe().putByte(addr++, (byte) value);
91+
return addr;
92+
}
93+
7994
@Override
8095
public void close() {
8196
if (bufferPtr != 0) {
@@ -336,11 +351,7 @@ public void skip(int bytes) {
336351
}
337352

338353
private static void writeVarintDirect(long addr, long value) {
339-
while (value > 0x7F) {
340-
Unsafe.getUnsafe().putByte(addr++, (byte) ((value & 0x7F) | 0x80));
341-
value >>>= 7;
342-
}
343-
Unsafe.getUnsafe().putByte(addr, (byte) value);
354+
writeVarint(addr, value);
344355
}
345356

346357
private void encodeUtf8(CharSequence value, int utf8Len) {

core/src/main/java/io/questdb/client/cutlass/qwp/client/QwpWebSocketSender.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3362,6 +3362,17 @@ private void ensureConnected() {
33623362
cursorSendLoop.setConnectionDispatcher(connectionDispatcher);
33633363
cursorSendLoop.start();
33643364
} catch (Throwable t) {
3365+
// start() (or dispatcher construction) failed after cursorSendLoop was
3366+
// assigned. Close it so a caller that retries -- re-entering
3367+
// ensureConnected and reassigning cursorSendLoop above -- cannot orphan
3368+
// a recovered slot's ctor-seeded native mirror (freed only by close()
3369+
// or the I/O loop, neither of which has run). close() is idempotent and
3370+
// frees the mirror via its loopNeverRan path; it also closes the shared
3371+
// client, so the client.close() below is a safe idempotent no-op.
3372+
if (cursorSendLoop != null) {
3373+
cursorSendLoop.close();
3374+
cursorSendLoop = null;
3375+
}
33653376
if (client != null) {
33663377
client.close();
33673378
client = null;

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,15 @@ public MmapSegment firstSealed() {
619619
return ring.firstSealed();
620620
}
621621

622+
/**
623+
* The engine's persisted symbol dictionary, or {@code null} in memory mode
624+
* (and in disk mode if it failed to open). The producer appends new symbols
625+
* to it; recovery / orphan-drain read its loaded entries to seed catch-up.
626+
*/
627+
public PersistedSymbolDict getPersistedSymbolDict() {
628+
return persistedSymbolDict;
629+
}
630+
622631
/**
623632
* Number of times {@link #appendBlocking} hit
624633
* {@link SegmentRing#BACKPRESSURE_NO_SPARE} on its first attempt and
@@ -641,15 +650,6 @@ public boolean isDeltaDictEnabled() {
641650
return sfDir == null || persistedSymbolDict != null;
642651
}
643652

644-
/**
645-
* The engine's persisted symbol dictionary, or {@code null} in memory mode
646-
* (and in disk mode if it failed to open). The producer appends new symbols
647-
* to it; recovery / orphan-drain read its loaded entries to seed catch-up.
648-
*/
649-
public PersistedSymbolDict getPersistedSymbolDict() {
650-
return persistedSymbolDict;
651-
}
652-
653653
/**
654654
* Pass-through to {@link SegmentRing#nextSealedAfter(MmapSegment)}.
655655
*/

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

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2161,6 +2161,14 @@ private long readVarintAt(long p, long limit) {
21612161
break;
21622162
}
21632163
shift += 7;
2164+
if (shift > 35) {
2165+
// Defensive bound, matching PersistedSymbolDict.decodeVarint: a
2166+
// canonical entry-length / delta varint is <= 5 bytes. Every caller
2167+
// reads freshly-encoded, CRC- or openExisting-validated bytes, so
2168+
// this is unreachable, but it stops a corrupt continuation run from
2169+
// over-shifting into a garbage length.
2170+
break;
2171+
}
21642172
}
21652173
varintEnd = cur;
21662174
return value;
@@ -2310,8 +2318,8 @@ private void sendCatchUpChunk(int deltaStart, int deltaCount, long symbolsAddr,
23102318
| QwpConstants.FLAG_DEFER_COMMIT));
23112319
Unsafe.getUnsafe().putShort(frame + 6, (short) 0); // tableCount
23122320
Unsafe.getUnsafe().putInt(frame + 8, payloadLen);
2313-
long q = writeVarintAt(frame + QwpConstants.HEADER_SIZE, deltaStart);
2314-
q = writeVarintAt(q, deltaCount);
2321+
long q = NativeBufferWriter.writeVarint(frame + QwpConstants.HEADER_SIZE, deltaStart);
2322+
q = NativeBufferWriter.writeVarint(q, deltaCount);
23152323
Unsafe.getUnsafe().copyMemory(symbolsAddr, q, symbolsLen);
23162324
client.sendBinary(frame, frameLen);
23172325
} catch (Throwable t) {
@@ -2331,15 +2339,6 @@ private void sendCatchUpChunk(int deltaStart, int deltaCount, long symbolsAddr,
23312339
totalFramesSent.incrementAndGet();
23322340
}
23332341

2334-
private static long writeVarintAt(long addr, long value) {
2335-
while (value > 0x7F) {
2336-
Unsafe.getUnsafe().putByte(addr++, (byte) ((value & 0x7F) | 0x80));
2337-
value >>>= 7;
2338-
}
2339-
Unsafe.getUnsafe().putByte(addr++, (byte) value);
2340-
return addr;
2341-
}
2342-
23432342
private boolean tryReceiveAcks() {
23442343
boolean any = false;
23452344
try {

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

Lines changed: 29 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
package io.questdb.client.cutlass.qwp.client.sf.cursor;
2626

2727
import io.questdb.client.cutlass.qwp.client.GlobalSymbolDictionary;
28+
import io.questdb.client.cutlass.qwp.client.NativeBufferWriter;
2829
import io.questdb.client.std.Files;
2930
import io.questdb.client.std.MemoryTag;
3031
import io.questdb.client.std.ObjList;
@@ -218,10 +219,10 @@ public synchronized void appendSymbol(CharSequence symbol) {
218219
return;
219220
}
220221
int utf8Len = Utf8s.utf8Bytes(symbol);
221-
int varLen = varintSize(utf8Len);
222+
int varLen = NativeBufferWriter.varintSize(utf8Len);
222223
int recLen = varLen + utf8Len;
223224
ensureScratch(recLen);
224-
long p = writeVarint(scratchAddr, utf8Len);
225+
long p = NativeBufferWriter.writeVarint(scratchAddr, utf8Len);
225226
if (utf8Len > 0) {
226227
Utf8s.strCpyUtf8(symbol, p, utf8Len);
227228
}
@@ -257,9 +258,9 @@ public synchronized void appendSymbols(GlobalSymbolDictionary dict, int from, in
257258
for (int id = from; id <= to; id++) {
258259
CharSequence symbol = dict.getSymbol(id);
259260
int utf8Len = Utf8s.utf8Bytes(symbol);
260-
int recLen = varintSize(utf8Len) + utf8Len;
261+
int recLen = NativeBufferWriter.varintSize(utf8Len) + utf8Len;
261262
ensureScratch(len + recLen);
262-
long p = writeVarint(scratchAddr + len, utf8Len);
263+
long p = NativeBufferWriter.writeVarint(scratchAddr + len, utf8Len);
263264
if (utf8Len > 0) {
264265
Utf8s.strCpyUtf8(symbol, p, utf8Len);
265266
}
@@ -351,6 +352,30 @@ public int size() {
351352
return size;
352353
}
353354

355+
/**
356+
* Decodes an unsigned LEB128 varint from {@code buf[pos..limit)}. Returns
357+
* {@code [value, newPos]} or {@code null} if the varint is truncated
358+
* (torn tail).
359+
*/
360+
private static long[] decodeVarint(long buf, int pos, int limit) {
361+
long value = 0;
362+
int shift = 0;
363+
int cur = pos;
364+
while (cur < limit) {
365+
byte b = Unsafe.getUnsafe().getByte(buf + cur);
366+
cur++;
367+
value |= (long) (b & 0x7F) << shift;
368+
if ((b & 0x80) == 0) {
369+
return new long[]{value, cur};
370+
}
371+
shift += 7;
372+
if (shift > 35) {
373+
return null; // implausible for an entry length; treat as torn
374+
}
375+
}
376+
return null;
377+
}
378+
354379
private static PersistedSymbolDict openExisting(String filePath, long fileLen) {
355380
int fd = Files.openRW(filePath);
356381
if (fd < 0) {
@@ -473,48 +498,6 @@ private static PersistedSymbolDict openFresh(String filePath) {
473498
return new PersistedSymbolDict(fd, HEADER_SIZE, 0, 0L, 0);
474499
}
475500

476-
/**
477-
* Decodes an unsigned LEB128 varint from {@code buf[pos..limit)}. Returns
478-
* {@code [value, newPos]} or {@code null} if the varint is truncated
479-
* (torn tail).
480-
*/
481-
private static long[] decodeVarint(long buf, int pos, int limit) {
482-
long value = 0;
483-
int shift = 0;
484-
int cur = pos;
485-
while (cur < limit) {
486-
byte b = Unsafe.getUnsafe().getByte(buf + cur);
487-
cur++;
488-
value |= (long) (b & 0x7F) << shift;
489-
if ((b & 0x80) == 0) {
490-
return new long[]{value, cur};
491-
}
492-
shift += 7;
493-
if (shift > 35) {
494-
return null; // implausible for an entry length; treat as torn
495-
}
496-
}
497-
return null;
498-
}
499-
500-
private static int varintSize(long value) {
501-
int n = 1;
502-
while (value > 0x7F) {
503-
value >>>= 7;
504-
n++;
505-
}
506-
return n;
507-
}
508-
509-
private static long writeVarint(long addr, long value) {
510-
while (value > 0x7F) {
511-
Unsafe.getUnsafe().putByte(addr++, (byte) ((value & 0x7F) | 0x80));
512-
value >>>= 7;
513-
}
514-
Unsafe.getUnsafe().putByte(addr++, (byte) value);
515-
return addr;
516-
}
517-
518501
private void ensureScratch(int required) {
519502
if (scratchCap >= required) {
520503
return;

core/src/test/java/io/questdb/client/test/cutlass/qwp/client/sf/cursor/CursorWebSocketSendLoopCatchUpAlignmentTest.java

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import io.questdb.client.cutlass.qwp.client.sf.cursor.CursorSendEngine;
3131
import io.questdb.client.cutlass.qwp.client.sf.cursor.CursorWebSocketSendLoop;
3232
import io.questdb.client.network.PlainSocketFactory;
33-
import io.questdb.client.std.Files;
3433
import io.questdb.client.std.MemoryTag;
3534
import io.questdb.client.std.Unsafe;
3635
import io.questdb.client.test.tools.TestUtils;
@@ -42,7 +41,6 @@
4241
import java.lang.reflect.InvocationTargetException;
4342
import java.lang.reflect.Method;
4443
import java.nio.charset.StandardCharsets;
45-
import java.nio.file.Paths;
4644
import java.util.ArrayList;
4745
import java.util.Arrays;
4846
import java.util.List;
@@ -78,30 +76,12 @@ public class CursorWebSocketSendLoopCatchUpAlignmentTest {
7876

7977
@Before
8078
public void setUp() {
81-
tmpDir = Paths.get(System.getProperty("java.io.tmpdir"),
82-
"qdb-cursor-catchup-" + System.nanoTime()).toString();
83-
assertEquals(0, Files.mkdir(tmpDir, Files.DIR_MODE_DEFAULT));
79+
tmpDir = TestUtils.createTmpDir("qdb-cursor-catchup-");
8480
}
8581

8682
@After
8783
public void tearDown() {
88-
if (tmpDir == null) return;
89-
long find = Files.findFirst(tmpDir);
90-
if (find > 0) {
91-
try {
92-
int rc = 1;
93-
while (rc > 0) {
94-
String name = Files.utf8ToString(Files.findName(find));
95-
if (name != null && !".".equals(name) && !"..".equals(name)) {
96-
Files.remove(tmpDir + "/" + name);
97-
}
98-
rc = Files.findNext(find);
99-
}
100-
} finally {
101-
Files.findClose(find);
102-
}
103-
}
104-
Files.remove(tmpDir);
84+
TestUtils.removeTmpDir(tmpDir);
10585
}
10686

10787
@Test

0 commit comments

Comments
 (0)