Skip to content

Commit f3282a4

Browse files
committed
Harden delta dictionary test correctness
1 parent 5617d79 commit f3282a4

2 files changed

Lines changed: 94 additions & 11 deletions

File tree

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

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ public void testReconnectCatchUpRebuildsDictionary() throws Exception {
9797
Assert.assertEquals("2nd connection dictionary size", 2, conn2.size());
9898
Assert.assertEquals("alpha", conn2.get(0));
9999
Assert.assertEquals("beta", conn2.get(1));
100+
assertAckSequencesStartAtZero(handler.ackSequenceStarts());
100101
}
101102
});
102103
}
@@ -143,6 +144,7 @@ public void testReconnectPreservesMonotonicDeltaBaseline() throws Exception {
143144
+ "baseline (deltaStart >= 1); a reset baseline re-ships the whole "
144145
+ "dictionary from deltaStart 0",
145146
handler.conn2SawDeltaAboveBaseline);
147+
assertAckSequencesStartAtZero(handler.ackSequenceStarts());
146148
}
147149
});
148150
}
@@ -204,6 +206,7 @@ public void testFixedCapNearBoundarySymbolCatchesUpWithoutTerminal() throws Exce
204206
Assert.assertEquals("2nd connection dictionary size", 2, conn2.size());
205207
Assert.assertEquals(nearCapSymbol, conn2.get(0));
206208
Assert.assertEquals("beta", conn2.get(1));
209+
assertAckSequencesStartAtZero(handler.ackSequenceStarts());
207210
}
208211
});
209212
}
@@ -251,6 +254,7 @@ public void testForegroundCatchUpCapGapRetriesPastOrphanBudgetAndRecovers() thro
251254
Assert.assertTrue("foreground sender must recover and drain after the cap is restored",
252255
sender.awaitAckedFsn(targetFsn, 5_000));
253256
}
257+
assertAckSequencesStartAtZero(handler.ackSequenceStarts());
254258
}
255259
});
256260
}
@@ -307,10 +311,19 @@ public void testReconnectCatchUpSplitsLargeDictionaryAcrossFrames() throws Excep
307311
Assert.assertTrue("catch-up split into multiple frames (saw "
308312
+ handler.zeroTableFramesOnConn2 + ")",
309313
handler.zeroTableFramesOnConn2 >= 2);
314+
assertAckSequencesStartAtZero(handler.ackSequenceStarts());
310315
}
311316
});
312317
}
313318

319+
private static void assertAckSequencesStartAtZero(List<Long> ackSequenceStarts) {
320+
Assert.assertFalse("the fixture must ACK at least one connection", ackSequenceStarts.isEmpty());
321+
for (int i = 0; i < ackSequenceStarts.size(); i++) {
322+
Assert.assertEquals("connection " + (i + 1) + " must begin ACK sequencing at zero",
323+
0L, ackSequenceStarts.get(i).longValue());
324+
}
325+
}
326+
314327
private static String symbolName(int i) {
315328
// 10-char symbols so 40 of them clearly exceed the advertised 160-byte cap.
316329
return "symbol" + (1000 + i);
@@ -371,10 +384,15 @@ private static class CatchUpHandler implements TestWebSocketServer.WebSocketServ
371384
// post-reconnect frame trips it.
372385
volatile boolean conn2SawDeltaAboveBaseline;
373386
volatile boolean sawZeroTableFrameOnConn2;
387+
private final List<Long> ackSequenceStarts = new CopyOnWriteArrayList<>();
374388
private final List<List<String>> dictsByConn = new CopyOnWriteArrayList<>();
375389
private TestWebSocketServer.ClientHandler currentClient;
376390
private final AtomicLong nextSeq = new AtomicLong(0);
377391

392+
List<Long> ackSequenceStarts() {
393+
return new ArrayList<>(ackSequenceStarts);
394+
}
395+
378396
synchronized List<String> dictFor(int connNumber) {
379397
return connNumber <= dictsByConn.size()
380398
// Copy under the lock: the caller iterates it unlocked while the
@@ -390,6 +408,7 @@ public synchronized void onBinaryMessage(TestWebSocketServer.ClientHandler clien
390408
currentClient = client;
391409
connectionsAccepted.incrementAndGet();
392410
dictsByConn.add(new ArrayList<>()); // fresh dictionary per connection
411+
nextSeq.set(0);
393412
}
394413
int connNumber = dictsByConn.size();
395414
List<String> dict = dictsByConn.get(connNumber - 1);
@@ -414,7 +433,11 @@ public synchronized void onBinaryMessage(TestWebSocketServer.ClientHandler clien
414433
}
415434
}
416435
try {
417-
client.sendBinary(buildAck(nextSeq.getAndIncrement()));
436+
long ackSequence = nextSeq.getAndIncrement();
437+
if (newConnection) {
438+
ackSequenceStarts.add(ackSequence);
439+
}
440+
client.sendBinary(buildAck(ackSequence));
418441
// Drop the first connection right after ACKing its only frame,
419442
// forcing the sender to reconnect onto a fresh dictionary.
420443
if (connNumber == 1) {
@@ -474,23 +497,33 @@ private static int tableCount(byte[] frame) {
474497
*/
475498
private static class CapShrinkHandler implements TestWebSocketServer.WebSocketServerHandler {
476499
final AtomicInteger connectionsAccepted = new AtomicInteger();
500+
private final List<Long> ackSequenceStarts = new CopyOnWriteArrayList<>();
477501
private final AtomicLong nextSeq = new AtomicLong(0);
478502
private TestWebSocketServer.ClientHandler currentClient;
479503
private volatile TestWebSocketServer server;
480504

505+
List<Long> ackSequenceStarts() {
506+
return new ArrayList<>(ackSequenceStarts);
507+
}
508+
481509
void setServer(TestWebSocketServer server) {
482510
this.server = server;
483511
}
484512

485513
@Override
486514
public synchronized void onBinaryMessage(TestWebSocketServer.ClientHandler client, byte[] data) {
487-
if (currentClient != client) {
515+
boolean newConnection = currentClient != client;
516+
if (newConnection) {
488517
currentClient = client;
489518
connectionsAccepted.incrementAndGet();
490519
nextSeq.set(0);
491520
}
492521
try {
493-
client.sendBinary(CatchUpHandler.buildAck(nextSeq.getAndIncrement()));
522+
long ackSequence = nextSeq.getAndIncrement();
523+
if (newConnection) {
524+
ackSequenceStarts.add(ackSequence);
525+
}
526+
client.sendBinary(CatchUpHandler.buildAck(ackSequence));
494527
if (connectionsAccepted.get() == 1) {
495528
// Connection 1 registered the big symbol. Shrink the cap so the
496529
// reconnect's catch-up budget can't fit it, then drop to force
@@ -518,6 +551,7 @@ private static class SplitCatchUpHandler implements TestWebSocketServer.WebSocke
518551
// Set once the server has closed connection 1 (see CatchUpHandler.conn1Closed).
519552
volatile boolean conn1Closed;
520553
volatile int zeroTableFramesOnConn2;
554+
private final List<Long> ackSequenceStarts = new CopyOnWriteArrayList<>();
521555
private final List<List<String>> dictsByConn = new CopyOnWriteArrayList<>();
522556
private final int dropConn1AtDictSize;
523557
private final AtomicLong nextSeq = new AtomicLong(0);
@@ -528,6 +562,10 @@ private static class SplitCatchUpHandler implements TestWebSocketServer.WebSocke
528562
this.dropConn1AtDictSize = dropConn1AtDictSize;
529563
}
530564

565+
List<Long> ackSequenceStarts() {
566+
return new ArrayList<>(ackSequenceStarts);
567+
}
568+
531569
synchronized List<String> dictFor(int connNumber) {
532570
return connNumber <= dictsByConn.size()
533571
// Copy under the lock: the caller iterates it unlocked while the
@@ -543,6 +581,7 @@ public synchronized void onBinaryMessage(TestWebSocketServer.ClientHandler clien
543581
currentClient = client;
544582
connectionsAccepted.incrementAndGet();
545583
dictsByConn.add(new ArrayList<>()); // fresh dictionary per connection
584+
nextSeq.set(0);
546585
}
547586
int connNumber = dictsByConn.size();
548587
List<String> dict = dictsByConn.get(connNumber - 1);
@@ -551,7 +590,11 @@ public synchronized void onBinaryMessage(TestWebSocketServer.ClientHandler clien
551590
zeroTableFramesOnConn2++;
552591
}
553592
try {
554-
client.sendBinary(CatchUpHandler.buildAck(nextSeq.getAndIncrement()));
593+
long ackSequence = nextSeq.getAndIncrement();
594+
if (newConnection) {
595+
ackSequenceStarts.add(ackSequence);
596+
}
597+
client.sendBinary(CatchUpHandler.buildAck(ackSequence));
555598
// Drop connection 1 only once it has learned the entire batch, so
556599
// the sender's sent-dictionary mirror is complete and the reconnect
557600
// catch-up must replay a dictionary larger than the batch cap.

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

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -789,8 +789,9 @@ public void testPersistFailureSurfacesAsLineSenderException() throws Exception {
789789
CursorSendEngine engine = new CursorSendEngine(
790790
slot, 4L * 1024 * 1024, CursorSendEngine.DEFAULT_APPEND_DEADLINE_NANOS,
791791
CursorSendEngine.DEFAULT_APPEND_DEADLINE_NANOS, ff);
792-
try (Sender sender = QwpWebSocketSender.connect(
793-
"localhost", port, null, 0, 0, 0L, null, false, engine)) {
792+
Sender sender = QwpWebSocketSender.connect(
793+
"localhost", port, null, 0, 0, 0L, null, false, engine);
794+
try {
794795
ff.armed = true; // the next dictionary append short-writes
795796
sender.table("m").symbol("s", "boom").longColumn("v", 1L).atNow();
796797
try {
@@ -801,14 +802,42 @@ public void testPersistFailureSurfacesAsLineSenderException() throws Exception {
801802
+ "not leak a raw IllegalStateException: " + expected.getMessage(),
802803
expected.getMessage().contains("failed to persist symbol dictionary before publish"));
803804
}
804-
} catch (LineSenderException ignored) {
805-
// close() re-flushes the still-buffered row; the facade has disarmed, so
806-
// this normally succeeds. Either way it is not what we assert.
805+
} finally {
806+
try {
807+
sender.close();
808+
} catch (LineSenderException ignored) {
809+
// close() re-flushes the still-buffered row; the facade has disarmed, so
810+
// this normally succeeds. Either way it is not what we assert.
811+
}
807812
}
808813
}
809814
});
810815
}
811816

817+
@Test
818+
public void testReconstructingFixtureResetsAckSequencePerConnection() throws Exception {
819+
assertMemoryLeak(() -> {
820+
DictReconstructingHandler handler = new DictReconstructingHandler();
821+
try (TestWebSocketServer server = new TestWebSocketServer(handler)) {
822+
int port = server.getPort();
823+
server.start();
824+
Assert.assertTrue(server.awaitStart(5, TimeUnit.SECONDS));
825+
826+
for (int i = 0; i < 2; i++) {
827+
try (Sender sender = Sender.fromConfig("ws::addr=localhost:" + port + ";")) {
828+
sender.table("m").symbol("s", "sym-" + i).longColumn("v", i).atNow();
829+
long fsn = sender.flushAndGetSequence();
830+
Assert.assertTrue("connection " + (i + 1) + " must receive ACK zero",
831+
sender.awaitAckedFsn(fsn, 5_000));
832+
}
833+
}
834+
835+
Assert.assertEquals("each fresh connection must restart wire ACK sequencing",
836+
Arrays.asList(0L, 0L), handler.ackSequenceStarts());
837+
}
838+
});
839+
}
840+
812841
@Test
813842
public void testFailedPublishDoesNotDuplicatePersistedSymbols() throws Exception {
814843
// Regression: persistNewSymbolsBeforePublish is a write-ahead -- it runs
@@ -1385,10 +1414,15 @@ private static int readVarint(byte[] buf, int[] pos) {
13851414
*/
13861415
private static class DictReconstructingHandler implements TestWebSocketServer.WebSocketServerHandler {
13871416
volatile boolean sawCatchUpFrame;
1417+
private final List<Long> ackSequenceStarts = new ArrayList<>();
13881418
private final List<String> dict = new ArrayList<>();
13891419
private final AtomicLong nextSeq = new AtomicLong(0);
13901420
private TestWebSocketServer.ClientHandler currentClient;
13911421

1422+
synchronized List<Long> ackSequenceStarts() {
1423+
return new ArrayList<>(ackSequenceStarts);
1424+
}
1425+
13921426
synchronized List<String> dictSnapshot() {
13931427
return new ArrayList<>(dict);
13941428
}
@@ -1399,16 +1433,22 @@ synchronized int maxDictSize() {
13991433

14001434
@Override
14011435
public synchronized void onBinaryMessage(TestWebSocketServer.ClientHandler client, byte[] data) {
1402-
if (currentClient != client) {
1436+
boolean newConnection = currentClient != client;
1437+
if (newConnection) {
14031438
currentClient = client;
14041439
dict.clear(); // fresh server dictionary per connection
1440+
nextSeq.set(0);
14051441
}
14061442
accumulate(data);
14071443
if (tableCount(data) == 0 && hasDelta(data)) {
14081444
sawCatchUpFrame = true;
14091445
}
14101446
try {
1411-
client.sendBinary(buildAck(nextSeq.getAndIncrement()));
1447+
long ackSequence = nextSeq.getAndIncrement();
1448+
if (newConnection) {
1449+
ackSequenceStarts.add(ackSequence);
1450+
}
1451+
client.sendBinary(buildAck(ackSequence));
14121452
} catch (IOException e) {
14131453
throw new RuntimeException(e);
14141454
}

0 commit comments

Comments
 (0)