Skip to content

Commit 6d007c7

Browse files
glasstigerclaude
andcommitted
Close test loops, drop dead walkers, fix misleading comments
Review follow-ups: resource hygiene in the poison-frame tests, dead code left behind by the recovery fold, and comments that contradict the code. Wrap the twelve CursorWebSocketSendLoops in CursorWebSocketSendLoopPoisonFrameTest in try-with-resources. They were never closed, and their enclosing bodies leaked on the assertion-failure path too, which TestUtils.assertMemoryLeak hides because it calls skipChecks() when the body throws. Nesting the loop inside the engine's try-with-resources also fixes close order: the loop borrows the engine's persisted dictionary prefix, so it must close first. A probe on all twelve sites confirms none of them actually leaked -- no symbol-dict mirror or catch-up frame buffer was allocated on these paths, and the reconnect factory registers every client in the list closeAll() drains. The value is in removing the trap: releaseSentDictBytes() and freeCatchUpFrameBuffer() are reachable only through close(), so a future test in one of these bodies that triggers a dictionary catch-up would have leaked silently. Carry the slot-qualified path into PersistedSymbolDict so the mmap-trim warning in close() names the dictionary it is about. It logged the bare FILE_NAME constant, which is unattributable in a process holding many slots -- a foreground sender plus N orphan drainers. Delete SegmentRing.findLastFsnWithoutPayloadFlag and MmapSegment.findLastFrameFsnWithoutPayloadFlag. The recovery fold left both without callers, and each walks the whole backlog. Drop the two dead stores in RecoveredFrameAnalysis.finish(): only appendRaw() reads those fields, and it cannot run after finish(). Correct four comments that state the opposite of what the code does: - hasEverConnected is documented as observability only while being half of endpointPolicyFailureIsTerminal(). - PersistedSymbolDict.close() claims that nulling the pointer prevents use-after-free, contradicting loadedEntriesAddr()'s accurate javadoc. It guards a construction-phase reader; ordering is what makes the borrow safe across threads. - sentDictBytesOwned describes borrowing as an orphan-only state, but loops of either policy borrow now. - connectWithDurableAckRetry refers to @testonly callers that no longer exist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ba4d458 commit 6d007c7

7 files changed

Lines changed: 278 additions & 324 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ public BackgroundDrainer() {
267267
*/
268268
public WebSocketClient connectWithDurableAckRetry() {
269269
// run() already set runnerThread; setting it again here is a no-op
270-
// on that path but wires up direct @TestOnly calls so requestStop()
270+
// on that path but wires up direct callers so requestStop()
271271
// can unpark them too.
272272
runnerThread = Thread.currentThread();
273273
long backoffMillis = reconnectInitialBackoffMillis;

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,10 @@ public final class CursorWebSocketSendLoop implements QuietCloseable {
299299
private long sentDictBytesAddr;
300300
private int sentDictBytesCapacity;
301301
private int sentDictBytesLen;
302-
// False only while an orphan-drainer loop borrows the persisted prefix.
303-
// Any growth first performs a copy-on-write; only owned buffers are freed.
302+
// False while the mirror is unallocated, and while a loop of EITHER policy borrows
303+
// the engine's persisted prefix -- both borrow now; neither consumes it. Any growth
304+
// copy-on-writes into loop-owned memory and sets this true; only owned buffers are
305+
// freed.
304306
private boolean sentDictBytesOwned;
305307
private int sentDictCount;
306308
// True when replay frames can start above dictionary id zero and therefore
@@ -373,8 +375,12 @@ public final class CursorWebSocketSendLoop implements QuietCloseable {
373375
private long fsnAtZero;
374376
// Sticky flag: false until the very first time a live client is installed
375377
// (either via the constructor in SYNC/OFF mode or via swapClient on a
376-
// successful connect attempt in any mode). Once true, stays true. This is
377-
// connection-state observability only; reconnect policy is role-based.
378+
// successful connect attempt in any mode). Once true, stays true.
379+
// LOAD-BEARING, not observability: it is half of endpointPolicyFailureIsTerminal(),
380+
// which latches an auth / upgrade / durable-ack rejection only for an ORPHAN drainer
381+
// or a sender that has never connected. Relocate or lazily initialise the write and a
382+
// foreground sender's transient auth blip becomes a producer-fatal terminal -- the
383+
// exact failure this policy exists to prevent.
378384
private volatile boolean hasEverConnected;
379385
// Cause of the outage the reconnect loop is currently riding out, or null once a
380386
// connect succeeds. Written by the I/O thread, read by the producer thread so a

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

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -566,47 +566,6 @@ public long tryAppend(long payloadAddr, int payloadLen) {
566566
return offset;
567567
}
568568

569-
/**
570-
* Walks every published frame in this segment and returns the FSN of the
571-
* LAST frame whose payload does NOT carry the given flag bit, or {@code -1}
572-
* when every frame carries it (or the segment is empty).
573-
* <p>
574-
* A frame counts as carrying the flag ONLY when it positively parses as a
575-
* message of the expected protocol: payload at least {@code minPayloadLen}
576-
* bytes AND the little-endian u32 at payload offset 0 equals
577-
* {@code headerMagic} AND the byte at {@code flagsOffset} has
578-
* {@code flagMask} set. Anything else -- short frames, foreign payloads,
579-
* magic mismatches -- counts as NOT carrying the flag. This direction is
580-
* deliberate: the caller retires (trims) frames ABOVE the returned FSN,
581-
* so a frame we cannot positively identify must act as a retirement
582-
* barrier, never as trimmable. Misclassifying an unknown frame as
583-
* deferred would silently discard data that should replay.
584-
* <p>
585-
* Producer-thread only, and only meaningful before new appends race the
586-
* walk (recovery time). Used to locate the last commit-bearing QWP frame
587-
* below a potentially orphaned FLAG_DEFER_COMMIT tail: frames above the
588-
* returned FSN all carry the flag, i.e. they belong to a transaction whose
589-
* commit frame was never published.
590-
*/
591-
public long findLastFrameFsnWithoutPayloadFlag(int flagsOffset, int flagMask, int headerMagic, int minPayloadLen) {
592-
long best = -1L;
593-
long off = HEADER_SIZE;
594-
long frames = frameCount;
595-
for (long i = 0; i < frames; i++) {
596-
int payloadLen = Unsafe.getUnsafe().getInt(mmapAddress + off + 4);
597-
long payload = mmapAddress + off + FRAME_HEADER_SIZE;
598-
boolean flagSet = payloadLen >= minPayloadLen
599-
&& payloadLen > flagsOffset
600-
&& Unsafe.getUnsafe().getInt(payload) == headerMagic
601-
&& (Unsafe.getUnsafe().getByte(payload + flagsOffset) & flagMask) != 0;
602-
if (!flagSet) {
603-
best = baseSeq + i;
604-
}
605-
off += FRAME_HEADER_SIZE + payloadLen;
606-
}
607-
return best;
608-
}
609-
610569
/**
611570
* Feeds every recovered frame in this segment to the engine's single-pass
612571
* recovery fold. Segments are visited oldest-first by {@link SegmentRing}.

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,11 @@ public final class PersistedSymbolDict implements QuietCloseable {
172172
// tests inject a fault facade to exercise recovery I/O failures (a truncate
173173
// that cannot drop a torn tail, a short write) without a real broken disk.
174174
private final FilesFacade ff;
175+
// Slot-qualified path, retained purely so diagnostics can name WHICH dictionary they
176+
// are about: one JVM routinely holds many (a foreground sender plus N orphan
177+
// drainers), so a warning carrying only the bare filename is unattributable exactly
178+
// when it fires.
179+
private final String filePath;
175180
// Production writes directly into segmented append mappings. Wrapping facades retain the
176181
// positioned-write path by default so fault tests can inject short writes through ff.write;
177182
// mmap-specific fault facades opt in through FilesFacade.isMmapAllowed().
@@ -209,6 +214,7 @@ public final class PersistedSymbolDict implements QuietCloseable {
209214

210215
private PersistedSymbolDict(
211216
FilesFacade ff,
217+
String filePath,
212218
int fd,
213219
long appendOffset,
214220
int size,
@@ -217,6 +223,7 @@ private PersistedSymbolDict(
217223
boolean mappedRecoveryInput
218224
) {
219225
this.ff = ff;
226+
this.filePath = filePath;
220227
this.fd = fd;
221228
this.mappedAppend = ff.isMmapAllowed();
222229
this.mappedRecoveryInput = mappedRecoveryInput;
@@ -510,9 +517,13 @@ public synchronized void close() {
510517
Unsafe.free(loadedEntriesAddr, loadedEntriesLen, MemoryTag.NATIVE_DEFAULT);
511518
} catch (Throwable ignored) {
512519
}
513-
// Null after freeing (like scratchAddr below) so a future accessor that
514-
// reads loadedEntriesAddr()/loadedEntriesLen() post-close cannot
515-
// dereference freed native memory; the getters are not closed-guarded.
520+
// Null after freeing (like scratchAddr below) so a CONSTRUCTION-PHASE reader
521+
// that runs after close observes 0 rather than a dangling pointer. This is
522+
// NOT a cross-thread guard, and the getters' own javadoc is the accurate
523+
// statement: they are unsynchronised reads of non-volatile fields, so a
524+
// caller on another thread has no guarantee of seeing this write and can
525+
// still dereference freed memory. Ordering, not nulling, is what makes the
526+
// borrow safe -- every owner closes its loop before the engine.
516527
loadedEntriesAddr = 0L;
517528
loadedEntriesLen = 0;
518529
}
@@ -533,7 +544,7 @@ public synchronized void close() {
533544
if (!ff.truncate(fd, appendOffset)) {
534545
LOG.warn("symbol dict {} could not trim mmap reserve to {}; recovery will "
535546
+ "discard the zero-filled tail on the next open",
536-
FILE_NAME, appendOffset);
547+
filePath, appendOffset);
537548
}
538549
} catch (Throwable ignored) {
539550
}
@@ -753,7 +764,7 @@ private static PersistedSymbolDict openExisting(FilesFacade ff, String filePath,
753764
throw new IllegalStateException("could not drop torn/stale symbol dictionary tail");
754765
}
755766
return new PersistedSymbolDict(
756-
ff, fd, scan.validLen, scan.count, entriesAddr, entriesLen, mappedInput);
767+
ff, filePath, fd, scan.validLen, scan.count, entriesAddr, entriesLen, mappedInput);
757768
} catch (Throwable t) {
758769
if (inputAddr != 0L) {
759770
if (mappedInput) {
@@ -921,7 +932,7 @@ private static PersistedSymbolDict openFresh(FilesFacade ff, String filePath) {
921932
Unsafe.free(hdr, HEADER_SIZE, MemoryTag.NATIVE_DEFAULT);
922933
}
923934
}
924-
return new PersistedSymbolDict(ff, fd, HEADER_SIZE, 0, 0L, 0, false);
935+
return new PersistedSymbolDict(ff, filePath, fd, HEADER_SIZE, 0, 0L, 0, false);
925936
}
926937

927938
/**

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ void accept(long fsn, long payload, int payloadLen) {
8787

8888
// Only a positively identified deferred QWP frame can belong to an
8989
// uncommitted tail. Short, foreign or otherwise non-QWP payloads remain
90-
// retirement barriers, matching findLastFrameFsnWithoutPayloadFlag.
90+
// retirement barriers.
9191
if (!isQwp || (flags & QwpConstants.FLAG_DEFER_COMMIT) == 0) {
9292
committedBoundaryFsn = fsn;
9393
committedCoverage = runningCoverage;
@@ -159,8 +159,6 @@ void finish() {
159159
committedRawCount = 0;
160160
committedGap = true;
161161
}
162-
runningRawLen = committedRawLen;
163-
runningRawCount = committedRawCount;
164162
if (rawCapacity == committedRawLen) {
165163
return;
166164
}

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

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -656,35 +656,6 @@ public synchronized MmapSegment firstSealed() {
656656
}
657657

658658
/** Active segment -- exposed for the I/O thread's "send next batch" path. */
659-
/**
660-
* Walks every published frame in the ring (sealed segments plus the active
661-
* segment) and returns the FSN of the LAST frame whose payload does NOT
662-
* carry the given flag bit, or {@code -1} when every published frame
663-
* carries it (or the ring is empty). All frames above the returned FSN
664-
* carry the flag.
665-
* <p>
666-
* Recovery-time helper: locates the last commit-bearing QWP frame below a
667-
* potentially orphaned FLAG_DEFER_COMMIT tail left behind by a producer
668-
* that crashed (or closed) mid-transaction. Call before the I/O loop and
669-
* producer start appending; the walk is not synchronized against appends
670-
* into the active segment. See
671-
* {@link MmapSegment#findLastFrameFsnWithoutPayloadFlag} for the
672-
* positive-identification contract: frames that do not parse as protocol
673-
* messages count as commit-bearing (retirement barriers), never as
674-
* trimmable.
675-
*/
676-
public synchronized long findLastFsnWithoutPayloadFlag(int flagsOffset, int flagMask, int headerMagic, int minPayloadLen) {
677-
long best = -1L;
678-
for (int i = sealedHead, n = sealedSegments.size(); i < n; i++) {
679-
long fsn = sealedSegments.get(i).findLastFrameFsnWithoutPayloadFlag(flagsOffset, flagMask, headerMagic, minPayloadLen);
680-
if (fsn > best) {
681-
best = fsn;
682-
}
683-
}
684-
long fsn = active.findLastFrameFsnWithoutPayloadFlag(flagsOffset, flagMask, headerMagic, minPayloadLen);
685-
return Math.max(best, fsn);
686-
}
687-
688659
/**
689660
* Performs the one ordered recovery fold across sealed segments and the
690661
* active segment. The returned native suffix remains owned by the caller.

0 commit comments

Comments
 (0)