Skip to content

Commit a167e65

Browse files
committed
test(client): replace reflection with typed test seams
1 parent b969bd9 commit a167e65

22 files changed

Lines changed: 716 additions & 599 deletions

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1292,6 +1292,11 @@ public void setSlotLockReleaseListener(Runnable listener) {
12921292
}
12931293
}
12941294

1295+
@TestOnly
1296+
public void setSlotLockReleasedForTesting(boolean isReleased) {
1297+
slotLockReleased = isReleased;
1298+
}
1299+
12951300
@Override
12961301
public Sender decimalColumn(CharSequence name, Decimal64 value) {
12971302
checkNotClosed();
@@ -1710,16 +1715,31 @@ public SenderConnectionDispatcher getConnectionDispatcherForTesting() {
17101715
return connectionDispatcher;
17111716
}
17121717

1718+
@TestOnly
1719+
public CursorSendEngine getCursorEngineForTesting() {
1720+
return cursorEngine;
1721+
}
1722+
17131723
@TestOnly
17141724
public SenderErrorDispatcher getErrorDispatcherForTesting() {
17151725
return errorDispatcher;
17161726
}
17171727

1728+
@TestOnly
1729+
public Sender.InitialConnectMode getInitialConnectModeForTesting() {
1730+
return initialConnectMode;
1731+
}
1732+
17181733
@TestOnly
17191734
public SenderProgressDispatcher getProgressDispatcherForTesting() {
17201735
return progressDispatcher;
17211736
}
17221737

1738+
@TestOnly
1739+
public Runnable getSlotLockReleaseListenerForTesting() {
1740+
return slotLockReleaseListener;
1741+
}
1742+
17231743
/**
17241744
* Number of {@link SenderError} notifications dropped because the
17251745
* bounded inbox was full. Non-zero means the user-supplied
@@ -2151,6 +2171,11 @@ public void setClientForTesting(WebSocketClient client) {
21512171
this.client = client;
21522172
}
21532173

2174+
@TestOnly
2175+
public void setClosedForTesting(boolean isClosed) {
2176+
this.closed = isClosed;
2177+
}
2178+
21542179
/**
21552180
* Installs a one-shot test witness that close-time drain invokes after it
21562181
* observes a real unacknowledged target and before it starts waiting.

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

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import io.questdb.client.std.MemoryTag;
3131
import io.questdb.client.std.QuietCloseable;
3232
import io.questdb.client.std.Unsafe;
33+
import org.jetbrains.annotations.TestOnly;
3334
import org.slf4j.Logger;
3435
import org.slf4j.LoggerFactory;
3536

@@ -102,6 +103,7 @@ public final class AckWatermark implements QuietCloseable {
102103
private boolean closed;
103104
private long fsn;
104105
private long generation;
106+
private boolean isStorageReleased;
105107

106108
private AckWatermark(FilesFacade filesFacade, int fd, long mmapAddress,
107109
long generation, long fsn) {
@@ -116,12 +118,7 @@ private AckWatermark(FilesFacade filesFacade, int fd, long mmapAddress,
116118
public void close() {
117119
if (closed) return;
118120
closed = true;
119-
if (mmapAddress != 0L && mmapAddress != Files.FAILED_MMAP_ADDRESS) {
120-
Files.munmap(mmapAddress, FILE_SIZE, MemoryTag.MMAP_DEFAULT);
121-
}
122-
if (fd >= 0) {
123-
filesFacade.close(fd);
124-
}
121+
releaseStorage();
125122
}
126123

127124
/**
@@ -171,6 +168,16 @@ static AckWatermark open(FilesFacade filesFacade, String slotDir) {
171168
: new AckWatermark(filesFacade, fd, addr, selected.generation, selected.fsn);
172169
}
173170

171+
/**
172+
* Releases the native storage while deliberately leaving the logical
173+
* closed flag clear. Test-only: recreates a stale racing writer without
174+
* reflective access to descriptor and mapping internals.
175+
*/
176+
@TestOnly
177+
public boolean releaseStorageButKeepWritableForTesting() {
178+
return releaseStorage();
179+
}
180+
174181
/**
175182
* Best-effort removal of a stale watermark file. Used by the engine
176183
* startup path when no segments are recovered.
@@ -240,6 +247,20 @@ public void write(long fsn) {
240247
this.fsn = fsn;
241248
}
242249

250+
private boolean releaseStorage() {
251+
if (isStorageReleased) {
252+
return false;
253+
}
254+
isStorageReleased = true;
255+
if (mmapAddress != 0L && mmapAddress != Files.FAILED_MMAP_ADDRESS) {
256+
Files.munmap(mmapAddress, FILE_SIZE, MemoryTag.MMAP_DEFAULT);
257+
}
258+
if (fd >= 0) {
259+
filesFacade.close(fd);
260+
}
261+
return true;
262+
}
263+
243264
private static Record readRecord(long address) {
244265
if (Unsafe.getUnsafe().getInt(address + MAGIC_OFFSET) != FILE_MAGIC
245266
|| Unsafe.getUnsafe().getInt(address + 4) != VERSION) {

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,16 @@ private void finishClose(boolean fullyDrained) {
900900
}
901901
}
902902

903+
@TestOnly
904+
public SegmentManager getManagerForTesting() {
905+
return manager;
906+
}
907+
908+
@TestOnly
909+
public SlotLock getSlotLockForTesting() {
910+
return slotLock;
911+
}
912+
903913
/**
904914
* Installs a hook invoked after each failed shared-driver flock release.
905915
* Test-only: makes persistent retry progress observable without sleeps.

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,31 @@ public void register(SegmentRing ring, String dir, AckWatermark watermark) {
583583
wakeWorker();
584584
}
585585

586+
@TestOnly
587+
public SegmentRing getInServiceRingForTesting() {
588+
RingEntry entry = inService;
589+
return entry == null ? null : entry.ring;
590+
}
591+
592+
@TestOnly
593+
public long getTotalBytesForTesting() {
594+
synchronized (lock) {
595+
return totalBytes;
596+
}
597+
}
598+
599+
@TestOnly
600+
public Thread getWorkerThreadForTesting() {
601+
return workerThread;
602+
}
603+
604+
@TestOnly
605+
public boolean isPathScratchAllocatedForTesting() {
606+
synchronized (lock) {
607+
return !scratchFreed;
608+
}
609+
}
610+
586611
@TestOnly
587612
public void setAfterRingCleanupHook(Runnable hook) {
588613
this.afterRingCleanupHook = hook;

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,6 +1229,11 @@ synchronized int pendingTrimCount() {
12291229
return pendingTrims.size();
12301230
}
12311231

1232+
@TestOnly
1233+
public synchronized MmapSegment getHotSpareForTesting() {
1234+
return hotSpare;
1235+
}
1236+
12321237
@TestOnly
12331238
public synchronized int getPendingTrimCount() {
12341239
return pendingTrims.size();

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import io.questdb.client.std.MemoryTag;
3030
import io.questdb.client.std.QuietCloseable;
3131
import io.questdb.client.std.Unsafe;
32+
import org.jetbrains.annotations.TestOnly;
3233

3334
import java.nio.charset.StandardCharsets;
3435

@@ -56,6 +57,7 @@
5657
*/
5758
public final class SlotLock implements QuietCloseable {
5859

60+
private static final int DEAD_FD_FOR_TESTING = 1_000_000_000;
5961
private static final String LOCK_FILE_NAME = ".lock";
6062
private static final String LOCK_PID_FILE_NAME = ".lock.pid";
6163
private final String slotDir;
@@ -111,6 +113,26 @@ public static SlotLock acquire(String slotDir) {
111113
}
112114
}
113115

116+
/**
117+
* Replaces the live descriptor with a known-dead value until the returned
118+
* guard closes. Test-only: exercises release retry paths without exposing
119+
* mutable descriptor state.
120+
*/
121+
@TestOnly
122+
public synchronized ReleaseFailureForTesting injectReleaseFailureForTesting() {
123+
if (fd < 0 || fd == DEAD_FD_FOR_TESTING) {
124+
throw new IllegalStateException("slot lock is not held by a live descriptor");
125+
}
126+
ReleaseFailureForTesting releaseFailure = new ReleaseFailureForTesting(fd);
127+
fd = DEAD_FD_FOR_TESTING;
128+
return releaseFailure;
129+
}
130+
131+
@TestOnly
132+
public synchronized boolean isReleaseFailureInjectedForTesting() {
133+
return fd == DEAD_FD_FOR_TESTING;
134+
}
135+
114136
/** Slot dir this lock guards. */
115137
public String slotDir() {
116138
return slotDir;
@@ -183,6 +205,13 @@ private static String readHolder(String pidPath) {
183205

184206
private static native int release0(int fd);
185207

208+
private synchronized void restoreFdForTesting(int savedFd) {
209+
if (fd != DEAD_FD_FOR_TESTING) {
210+
throw new IllegalStateException("slot lock release failure is not injected");
211+
}
212+
fd = savedFd;
213+
}
214+
186215
private static void writePid(String pidPath) {
187216
long pid;
188217
try {
@@ -212,4 +241,22 @@ private static void writePid(String pidPath) {
212241
Files.close(wfd);
213242
}
214243
}
244+
245+
@TestOnly
246+
public final class ReleaseFailureForTesting implements QuietCloseable {
247+
private final int savedFd;
248+
private boolean isRestored;
249+
250+
private ReleaseFailureForTesting(int savedFd) {
251+
this.savedFd = savedFd;
252+
}
253+
254+
@Override
255+
public synchronized void close() {
256+
if (!isRestored) {
257+
restoreFdForTesting(savedFd);
258+
isRestored = true;
259+
}
260+
}
261+
}
215262
}

core/src/main/java/io/questdb/client/impl/PooledSender.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import io.questdb.client.std.Decimal64;
3333
import io.questdb.client.std.bytes.DirectByteSlice;
3434
import org.jetbrains.annotations.NotNull;
35+
import org.jetbrains.annotations.TestOnly;
3536

3637
import java.time.Instant;
3738
import java.time.temporal.ChronoUnit;
@@ -380,6 +381,21 @@ long generation() {
380381
return generation;
381382
}
382383

384+
@TestOnly
385+
public Sender getDelegateForTesting() {
386+
return slot.delegate();
387+
}
388+
389+
@TestOnly
390+
public int getSlotIndexForTesting() {
391+
return slot.slotIndex();
392+
}
393+
394+
@TestOnly
395+
public boolean hasSameSlotForTesting(PooledSender that) {
396+
return slot == that.slot;
397+
}
398+
383399
SenderSlot slot() {
384400
return slot;
385401
}

core/src/main/java/io/questdb/client/impl/QueryClientPool.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,16 @@ void release(QueryWorker w, long gen) {
528528
}
529529
}
530530

531+
@TestOnly
532+
public boolean hasCreationWaiterForTesting() {
533+
lock.lock();
534+
try {
535+
return lock.hasWaiters(creationFinished);
536+
} finally {
537+
lock.unlock();
538+
}
539+
}
540+
531541
// White-box accessor for tests: reports the current in-flight creation count
532542
// under the pool lock. A non-zero value after a failed acquire() means the
533543
// slot reservation was never released -- the capacity-shrink bug this guards
@@ -542,6 +552,11 @@ public int inFlightCreations() {
542552
}
543553
}
544554

555+
@TestOnly
556+
public boolean isClosedForTesting() {
557+
return closed;
558+
}
559+
545560
private QueryWorker createUnlocked() {
546561
QwpQueryClient client = QwpQueryClient.fromConfig(configurationString);
547562
try {

core/src/main/java/io/questdb/client/impl/QuestDBImpl.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,16 @@ public synchronized void close() {
215215
closeQuietly(senderPool);
216216
}
217217

218+
@TestOnly
219+
public QueryClientPool getQueryPoolForTesting() {
220+
return queryPool;
221+
}
222+
223+
@TestOnly
224+
public SenderPool getSenderPoolForTesting() {
225+
return senderPool;
226+
}
227+
218228
private static void closeQuietly(PoolHousekeeper housekeeper) {
219229
if (housekeeper == null) {
220230
return;

0 commit comments

Comments
 (0)