Skip to content

Commit 16a3eb6

Browse files
committed
Merge branch 'vi_sf' of https://github.com/questdb/java-questdb-client into vi_sf
2 parents 13ea8a2 + ce92148 commit 16a3eb6

6 files changed

Lines changed: 84 additions & 15 deletions

File tree

ci/run_tests_pipeline.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,16 @@ stages:
8484
maven | "$(Agent.OS)"
8585
path: $(HOME)/.m2/repository
8686
displayName: "Cache Maven repository"
87-
- script: git clone --depth 1 https://github.com/questdb/questdb.git ./questdb
87+
- bash: |
88+
BRANCH="${SYSTEM_PULLREQUEST_SOURCEBRANCH:-$BUILD_SOURCEBRANCHNAME}"
89+
BRANCH="${BRANCH#refs/heads/}"
90+
if git ls-remote --exit-code --heads https://github.com/questdb/questdb.git "$BRANCH" >/dev/null 2>&1; then
91+
echo "Cloning matching questdb branch: $BRANCH"
92+
git clone --depth 1 --branch "$BRANCH" https://github.com/questdb/questdb.git ./questdb
93+
else
94+
echo "No matching questdb branch '$BRANCH', falling back to master"
95+
git clone --depth 1 https://github.com/questdb/questdb.git ./questdb
96+
fi
8897
displayName: git clone questdb
8998
- task: Maven@3
9099
displayName: "Update client version"

core/src/test/java/io/questdb/client/test/cutlass/qwp/client/CloseDrainTest.java

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

2727
import io.questdb.client.Sender;
28+
import io.questdb.client.cutlass.line.LineSenderException;
2829
import io.questdb.client.test.cutlass.qwp.websocket.TestWebSocketServer;
2930
import org.junit.Assert;
3031
import org.junit.Test;
@@ -142,8 +143,8 @@ public void testCloseFastWhenTimeoutIsMinusOne() throws Exception {
142143
@Test
143144
public void testCloseDrainTimesOutWhenAcksNeverArrive() throws Exception {
144145
// Server that buffers frames silently and never ACKs. close() must
145-
// return after roughly the configured timeout — not hang forever
146-
// and not return immediately.
146+
// throw a drain-timeout LineSenderException after roughly the
147+
// configured timeout — not hang forever and not return immediately.
147148
int port = TEST_PORT + 3;
148149
long timeoutMs = 500;
149150
SilentHandler handler = new SilentHandler();
@@ -154,12 +155,21 @@ public void testCloseDrainTimesOutWhenAcksNeverArrive() throws Exception {
154155
String cfg = "ws::addr=localhost:" + port
155156
+ ";close_flush_timeout_millis=" + timeoutMs + ";";
156157
long elapsedMs;
157-
try (Sender sender = Sender.fromConfig(cfg)) {
158+
Sender sender = Sender.fromConfig(cfg);
159+
try {
158160
sender.table("foo").longColumn("v", 1L).atNow();
159161
sender.flush();
160162
long t0 = System.nanoTime();
161-
sender.close();
163+
try {
164+
sender.close();
165+
Assert.fail("close() should have thrown a drain-timeout error");
166+
} catch (LineSenderException e) {
167+
Assert.assertTrue("expected drain-timeout message, got: " + e.getMessage(),
168+
e.getMessage().contains("drain timed out"));
169+
}
162170
elapsedMs = (System.nanoTime() - t0) / 1_000_000;
171+
} finally {
172+
sender.close(); // idempotent — closed flag is set on first call
163173
}
164174
Assert.assertTrue("close() returned too early: " + elapsedMs + "ms",
165175
elapsedMs >= timeoutMs);

core/src/test/java/io/questdb/client/test/cutlass/qwp/client/ReconnectTest.java

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

2727
import io.questdb.client.Sender;
28+
import io.questdb.client.cutlass.line.LineSenderException;
2829
import io.questdb.client.test.cutlass.qwp.websocket.TestWebSocketServer;
2930
import org.junit.Assert;
3031
import org.junit.Test;
@@ -119,7 +120,8 @@ public void testReconnectGivesUpAfterCap() throws Exception {
119120
+ ";reconnect_initial_backoff_millis=10"
120121
+ ";reconnect_max_backoff_millis=50"
121122
+ ";close_flush_timeout_millis=0;";
122-
try (Sender sender = Sender.fromConfig(cfg)) {
123+
Sender sender = Sender.fromConfig(cfg);
124+
try {
123125
sender.table("foo").longColumn("v", 1L).atNow();
124126
sender.flush();
125127

@@ -151,6 +153,13 @@ public void testReconnectGivesUpAfterCap() throws Exception {
151153
msg.contains("reconnect failed")
152154
|| msg.contains("I/O thread failed")
153155
|| msg.contains("Failed to connect"));
156+
} finally {
157+
// close() rethrows the latched terminal reconnect-cap error
158+
// (commit 052f6ee). Already observed and asserted above.
159+
try {
160+
sender.close();
161+
} catch (LineSenderException ignored) {
162+
}
154163
}
155164
} finally {
156165
try {
@@ -177,7 +186,8 @@ public void testTerminalUpgradeErrorAbortsReconnect() throws Exception {
177186
String cfg = "ws::addr=localhost:" + port
178187
+ ";reconnect_max_duration_millis=10000"
179188
+ ";close_flush_timeout_millis=0;";
180-
try (Sender sender = Sender.fromConfig(cfg)) {
189+
Sender sender = Sender.fromConfig(cfg);
190+
try {
181191
sender.table("foo").longColumn("v", 1L).atNow();
182192
sender.flush();
183193
// Wait for first connection to ACK + close
@@ -209,6 +219,13 @@ public void testTerminalUpgradeErrorAbortsReconnect() throws Exception {
209219
msg.contains("WebSocket upgrade failed")
210220
|| msg.contains("I/O thread failed")
211221
|| msg.contains("401"));
222+
} finally {
223+
// close() rethrows the latched terminal upgrade error
224+
// (commit 052f6ee). Already observed and asserted above.
225+
try {
226+
sender.close();
227+
} catch (LineSenderException ignored) {
228+
}
212229
}
213230
}
214231
}

core/src/test/java/io/questdb/client/test/cutlass/qwp/client/ServerErrorAckTerminalTest.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ public void testServerErrorAckIsTerminalAndDoesNotBurnReconnectBudget() throws E
8282
+ ";reconnect_max_backoff_millis=50"
8383
+ ";";
8484

85-
try (Sender sender = Sender.fromConfig(cfg)) {
85+
Sender sender = Sender.fromConfig(cfg);
86+
try {
8687
sender.table("foo").longColumn("v", 1L).atNow();
8788
sender.flush();
8889

@@ -125,6 +126,14 @@ public void testServerErrorAckIsTerminalAndDoesNotBurnReconnectBudget() throws E
125126
thrown.getMessage() != null
126127
&& (thrown.getMessage().contains("rejected")
127128
|| thrown.getMessage().contains("error")));
129+
} finally {
130+
// close() rethrows the latched terminal server-rejection error
131+
// (commit 052f6ee). Swallow it here — the test has already
132+
// observed and asserted on that error via flush() above.
133+
try {
134+
sender.close();
135+
} catch (LineSenderException ignored) {
136+
}
128137
}
129138
}
130139
}

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,16 @@ public void testScanFindsOrphanFromPriorSenderUnderSameGroupRoot() throws Except
119119
primary.table("foo").longColumn("v", 8L).atNow();
120120
primary.flush();
121121
}
122-
// Primary's slot now exists too; scanner with primary
123-
// excluded must still return the ghost (and nothing else
124-
// among the two slots).
122+
// With drain_orphans=true, the background drainer pool adopts
123+
// the ghost slot, replays its unacked frames against the now-
124+
// ACKing primaryServer, and removes the drained slot dir.
125+
// Primary's own slot drains cleanly on close() and is filtered
126+
// out by sender_id. Net: scanner sees neither.
125127
ObjList<String> postRun = OrphanScanner.scan(sfDir, "primary");
126-
Assert.assertEquals("only ghost should appear; primary excluded",
127-
1, postRun.size());
128-
Assert.assertEquals(sfDir + "/ghost", postRun.get(0));
128+
Assert.assertEquals(
129+
"drain_orphans=true should have drained + removed the "
130+
+ "ghost slot; primary's own slot is sender_id-filtered",
131+
0, postRun.size());
129132
} finally {
130133
try {
131134
primaryServer.close();

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
package io.questdb.client.test.cutlass.qwp.client.sf.cursor;
2626

2727
import io.questdb.client.cutlass.qwp.client.sf.cursor.CursorSendEngine;
28+
import io.questdb.client.cutlass.qwp.client.sf.cursor.SegmentManager;
29+
import io.questdb.client.cutlass.qwp.client.sf.cursor.SegmentRing;
2830
import io.questdb.client.cutlass.qwp.client.sf.cursor.SlotLock;
2931
import io.questdb.client.std.Files;
3032
import io.questdb.client.test.tools.TestUtils;
@@ -119,17 +121,36 @@ public void testSlotLockReleasedEvenIfRingCloseThrows() throws Exception {
119121
// Sabotage: zero out ring so engine.close() NPEs before reaching
120122
// the slotLock cleanup. Any close-path exception (manager.close,
121123
// ring.close, unlinkAllSegmentFiles) lands in the same place.
124+
//
125+
// Capture the ring + manager references first so we can free
126+
// their native resources ourselves after the sabotage — engine.close()
127+
// can no longer reach ring.close() / manager.close() once we null
128+
// the ring field, and assertMemoryLeak (+ the manager's worker
129+
// thread) would otherwise trip.
122130
Field ringField = CursorSendEngine.class.getDeclaredField("ring");
123131
ringField.setAccessible(true);
132+
SegmentRing capturedRing = (SegmentRing) ringField.get(engine);
133+
134+
Field managerField = CursorSendEngine.class.getDeclaredField("manager");
135+
managerField.setAccessible(true);
136+
SegmentManager capturedManager = (SegmentManager) managerField.get(engine);
137+
124138
ringField.set(engine, null);
125139

126140
try {
127141
engine.close();
128142
} catch (Throwable t) {
129-
// Expected — close() walks ring.close() and trips an NPE.
143+
// Expected — close() walks ring.publishedFsn() and trips an NPE.
130144
// The fix must release slotLock anyway, in finally.
131145
}
132146

147+
// Manually release the ring + manager resources that engine.close()
148+
// skipped because of the NPE. The slotLock contract is the only
149+
// thing the test is verifying; the rest of the close-path resources
150+
// are an artifact of the sabotage.
151+
capturedRing.close();
152+
capturedManager.close();
153+
133154
// The user-visible test: can a fresh SlotLock acquire the
134155
// same slot? If the original lock fd is still held, the
135156
// kernel's flock blocks this acquire and we throw.

0 commit comments

Comments
 (0)