Skip to content

Commit f12c934

Browse files
committed
Clean up QWP delta dictionary tests
1 parent 675d036 commit f12c934

4 files changed

Lines changed: 540 additions & 739 deletions

File tree

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

Lines changed: 8 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@
3131
import org.junit.Test;
3232

3333
import java.io.IOException;
34-
import java.nio.ByteBuffer;
35-
import java.nio.ByteOrder;
36-
import java.nio.charset.StandardCharsets;
3734
import java.util.ArrayList;
3835
import java.util.List;
3936
import java.util.concurrent.CopyOnWriteArrayList;
@@ -329,19 +326,6 @@ private static String symbolName(int i) {
329326
return "symbol" + (1000 + i);
330327
}
331328

332-
private static int readVarint(byte[] buf, int[] pos) {
333-
int result = 0;
334-
int shift = 0;
335-
while (pos[0] < buf.length) {
336-
int b = buf[pos[0]++] & 0xFF;
337-
result |= (b & 0x7F) << shift;
338-
if ((b & 0x80) == 0) return result;
339-
shift += 7;
340-
if (shift > 28) throw new IllegalStateException("varint too long");
341-
}
342-
throw new IllegalStateException("varint truncated");
343-
}
344-
345329
private static void waitFor(BoolCondition cond, long timeoutMillis) {
346330
long deadline = System.currentTimeMillis() + timeoutMillis;
347331
while (System.currentTimeMillis() < deadline) {
@@ -412,9 +396,9 @@ public synchronized void onBinaryMessage(TestWebSocketServer.ClientHandler clien
412396
}
413397
int connNumber = dictsByConn.size();
414398
List<String> dict = dictsByConn.get(connNumber - 1);
415-
accumulate(data, dict);
399+
QwpWireTestUtils.accumulateDeltaDictionary(data, dict);
416400
if (connNumber == 2) {
417-
if (tableCount(data) == 0) {
401+
if (QwpWireTestUtils.tableCount(data) == 0) {
418402
sawZeroTableFrameOnConn2 = true;
419403
// FLAG_DEFER_COMMIT is bit 0x01 of the flags byte (offset 5).
420404
catchUpDeferredOnConn2 = (data[5] & 0x01) != 0;
@@ -427,7 +411,7 @@ public synchronized void onBinaryMessage(TestWebSocketServer.ClientHandler clien
427411
// frame arriving ahead of the new one -- that replay carries its
428412
// original deltaStart 0 and does not trip the flag.
429413
int[] pos = {12};
430-
if (readVarint(data, pos) >= 1) {
414+
if (QwpWireTestUtils.readVarint(data, pos) >= 1) {
431415
conn2SawDeltaAboveBaseline = true;
432416
}
433417
}
@@ -437,7 +421,7 @@ public synchronized void onBinaryMessage(TestWebSocketServer.ClientHandler clien
437421
if (newConnection) {
438422
ackSequenceStarts.add(ackSequence);
439423
}
440-
client.sendBinary(buildAck(ackSequence));
424+
client.sendBinary(QwpWireTestUtils.buildAck(ackSequence));
441425
// Drop the first connection right after ACKing its only frame,
442426
// forcing the sender to reconnect onto a fresh dictionary.
443427
if (connNumber == 1) {
@@ -451,41 +435,6 @@ public synchronized void onBinaryMessage(TestWebSocketServer.ClientHandler clien
451435
}
452436
}
453437

454-
private static void accumulate(byte[] frame, List<String> dict) {
455-
final byte FLAG_DELTA_SYMBOL_DICT = 0x08;
456-
if (frame.length < 12 || (frame[5] & FLAG_DELTA_SYMBOL_DICT) == 0) {
457-
return;
458-
}
459-
int[] pos = {12}; // just past the 12-byte QWP header
460-
int deltaStart = readVarint(frame, pos);
461-
int deltaCount = readVarint(frame, pos);
462-
while (dict.size() < deltaStart) {
463-
dict.add(null); // null-pad, mirroring the server
464-
}
465-
for (int i = 0; i < deltaCount; i++) {
466-
int len = readVarint(frame, pos);
467-
String symbol = new String(frame, pos[0], len, StandardCharsets.UTF_8);
468-
pos[0] += len;
469-
int idx = deltaStart + i;
470-
while (dict.size() <= idx) {
471-
dict.add(null);
472-
}
473-
dict.set(idx, symbol);
474-
}
475-
}
476-
477-
private static byte[] buildAck(long seq) {
478-
byte[] buf = new byte[1 + 8 + 2];
479-
ByteBuffer bb = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN);
480-
bb.put((byte) 0x00);
481-
bb.putLong(seq);
482-
bb.putShort((short) 0);
483-
return buf;
484-
}
485-
486-
private static int tableCount(byte[] frame) {
487-
return (frame[6] & 0xFF) | ((frame[7] & 0xFF) << 8);
488-
}
489438
}
490439

491440
/**
@@ -523,7 +472,7 @@ public synchronized void onBinaryMessage(TestWebSocketServer.ClientHandler clien
523472
if (newConnection) {
524473
ackSequenceStarts.add(ackSequence);
525474
}
526-
client.sendBinary(CatchUpHandler.buildAck(ackSequence));
475+
client.sendBinary(QwpWireTestUtils.buildAck(ackSequence));
527476
if (connectionsAccepted.get() == 1) {
528477
// Connection 1 registered the big symbol. Shrink the cap so the
529478
// reconnect's catch-up budget can't fit it, then drop to force
@@ -585,16 +534,16 @@ public synchronized void onBinaryMessage(TestWebSocketServer.ClientHandler clien
585534
}
586535
int connNumber = dictsByConn.size();
587536
List<String> dict = dictsByConn.get(connNumber - 1);
588-
CatchUpHandler.accumulate(data, dict);
589-
if (connNumber == 2 && CatchUpHandler.tableCount(data) == 0) {
537+
QwpWireTestUtils.accumulateDeltaDictionary(data, dict);
538+
if (connNumber == 2 && QwpWireTestUtils.tableCount(data) == 0) {
590539
zeroTableFramesOnConn2++;
591540
}
592541
try {
593542
long ackSequence = nextSeq.getAndIncrement();
594543
if (newConnection) {
595544
ackSequenceStarts.add(ackSequence);
596545
}
597-
client.sendBinary(CatchUpHandler.buildAck(ackSequence));
546+
client.sendBinary(QwpWireTestUtils.buildAck(ackSequence));
598547
// Drop connection 1 only once it has learned the entire batch, so
599548
// the sender's sent-dictionary mirror is complete and the reconnect
600549
// 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: 23 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,8 @@
4141
import org.junit.Test;
4242

4343
import java.io.IOException;
44-
import java.lang.reflect.Field;
4544
import java.nio.ByteBuffer;
4645
import java.nio.ByteOrder;
47-
import java.nio.charset.StandardCharsets;
4846
import java.nio.file.Paths;
4947
import java.util.ArrayList;
5048
import java.util.Arrays;
@@ -1099,20 +1097,25 @@ public void testRecoverySeedKeepsUtf8CollidingSymbolsInLockstep() throws Excepti
10991097
}
11001098
Assert.assertEquals("two colliding symbols must persist as two entries", 2, persistedSize);
11011099

1102-
// Phase 2: recover. The seeded producer id space must match pd.size().
1100+
// Phase 2: recover, then introduce a third symbol. The wire must extend
1101+
// the two recovered ids at id 2 rather than overwrite id 1.
11031102
DictReconstructingHandler handler = new DictReconstructingHandler();
11041103
try (TestWebSocketServer good = new TestWebSocketServer(handler)) {
11051104
int port = good.getPort();
11061105
good.start();
11071106
Assert.assertTrue(good.awaitStart(5, TimeUnit.SECONDS));
11081107
String cfg = "ws::addr=localhost:" + port + ";sf_dir=" + sfDir + ";";
11091108
try (Sender s2 = Sender.fromConfig(cfg)) {
1110-
Assert.assertEquals("recovered producer dictionary must match the persisted "
1111-
+ "entry count (not de-duped), else the delta baseline desyncs from the mirror",
1112-
persistedSize, globalDictSize(s2));
1113-
Assert.assertEquals("delta baseline must resume at the persisted tip",
1114-
persistedSize - 1, intField(s2, "sentMaxSymbolId"));
1109+
s2.table("m").symbol("s", "tail").longColumn("v", 3L).atNow();
1110+
long fsn = s2.flushAndGetSequence();
1111+
Assert.assertTrue("the post-recovery frame must be acknowledged",
1112+
s2.awaitAckedFsn(fsn, 5_000));
11151113
}
1114+
Assert.assertEquals("the new symbol delta must continue after both recovered entries",
1115+
persistedSize, handler.lastDataDeltaStart());
1116+
Assert.assertEquals("the reconstructed dictionary must retain both colliding entries "
1117+
+ "and append the new symbol at id 2",
1118+
Arrays.asList("?", "?", "tail"), handler.dictSnapshot());
11161119
}
11171120
});
11181121
}
@@ -1369,19 +1372,6 @@ public void testFullDictFramesRecoverInFullDictModeInsteadOfBricking() throws Ex
13691372
});
13701373
}
13711374

1372-
private static int globalDictSize(Sender sender) throws Exception {
1373-
Field f = sender.getClass().getDeclaredField("globalSymbolDictionary");
1374-
f.setAccessible(true);
1375-
Object dict = f.get(sender);
1376-
return (int) dict.getClass().getMethod("size").invoke(dict);
1377-
}
1378-
1379-
private static int intField(Sender sender, String name) throws Exception {
1380-
Field f = sender.getClass().getDeclaredField(name);
1381-
f.setAccessible(true);
1382-
return f.getInt(sender);
1383-
}
1384-
13851375
private static void writeAckWatermark(java.nio.file.Path path, long fsn) throws IOException {
13861376
byte[] buf = new byte[16];
13871377
ByteBuffer bb = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN);
@@ -1391,23 +1381,6 @@ private static void writeAckWatermark(java.nio.file.Path path, long fsn) throws
13911381
java.nio.file.Files.write(path, buf);
13921382
}
13931383

1394-
private static int readVarint(byte[] buf, int[] pos) {
1395-
int result = 0;
1396-
int shift = 0;
1397-
while (pos[0] < buf.length) {
1398-
int b = buf[pos[0]++] & 0xFF;
1399-
result |= (b & 0x7F) << shift;
1400-
if ((b & 0x80) == 0) {
1401-
return result;
1402-
}
1403-
shift += 7;
1404-
if (shift > 28) {
1405-
throw new IllegalStateException("varint too long");
1406-
}
1407-
}
1408-
throw new IllegalStateException("varint truncated");
1409-
}
1410-
14111384
/**
14121385
* Reconstructs the per-connection symbol dictionary from delta sections,
14131386
* mirroring the server's {@code setQuick(deltaStart + i)} + null-padding.
@@ -1418,6 +1391,7 @@ private static class DictReconstructingHandler implements TestWebSocketServer.We
14181391
private final List<String> dict = new ArrayList<>();
14191392
private final AtomicLong nextSeq = new AtomicLong(0);
14201393
private TestWebSocketServer.ClientHandler currentClient;
1394+
private volatile int lastDataDeltaStart = -1;
14211395

14221396
synchronized List<Long> ackSequenceStarts() {
14231397
return new ArrayList<>(ackSequenceStarts);
@@ -1431,6 +1405,10 @@ synchronized int maxDictSize() {
14311405
return dict.size();
14321406
}
14331407

1408+
int lastDataDeltaStart() {
1409+
return lastDataDeltaStart;
1410+
}
1411+
14341412
@Override
14351413
public synchronized void onBinaryMessage(TestWebSocketServer.ClientHandler client, byte[] data) {
14361414
boolean newConnection = currentClient != client;
@@ -1439,59 +1417,23 @@ public synchronized void onBinaryMessage(TestWebSocketServer.ClientHandler clien
14391417
dict.clear(); // fresh server dictionary per connection
14401418
nextSeq.set(0);
14411419
}
1442-
accumulate(data);
1443-
if (tableCount(data) == 0 && hasDelta(data)) {
1420+
QwpWireTestUtils.accumulateDeltaDictionary(data, dict);
1421+
int tableCount = QwpWireTestUtils.tableCount(data);
1422+
if (tableCount == 0 && QwpWireTestUtils.hasDelta(data)) {
14441423
sawCatchUpFrame = true;
1424+
} else if (tableCount > 0 && QwpWireTestUtils.hasDelta(data)) {
1425+
lastDataDeltaStart = QwpWireTestUtils.readVarint(data, new int[]{12});
14451426
}
14461427
try {
14471428
long ackSequence = nextSeq.getAndIncrement();
14481429
if (newConnection) {
14491430
ackSequenceStarts.add(ackSequence);
14501431
}
1451-
client.sendBinary(buildAck(ackSequence));
1432+
client.sendBinary(QwpWireTestUtils.buildAck(ackSequence));
14521433
} catch (IOException e) {
14531434
throw new RuntimeException(e);
14541435
}
14551436
}
1456-
1457-
private static byte[] buildAck(long seq) {
1458-
byte[] buf = new byte[1 + 8 + 2];
1459-
ByteBuffer bb = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN);
1460-
bb.put((byte) 0x00);
1461-
bb.putLong(seq);
1462-
bb.putShort((short) 0);
1463-
return buf;
1464-
}
1465-
1466-
private static boolean hasDelta(byte[] frame) {
1467-
return frame.length >= 12 && (frame[5] & 0x08) != 0;
1468-
}
1469-
1470-
private static int tableCount(byte[] frame) {
1471-
return (frame[6] & 0xFF) | ((frame[7] & 0xFF) << 8);
1472-
}
1473-
1474-
private void accumulate(byte[] frame) {
1475-
if (!hasDelta(frame)) {
1476-
return;
1477-
}
1478-
int[] pos = {12};
1479-
int deltaStart = readVarint(frame, pos);
1480-
int deltaCount = readVarint(frame, pos);
1481-
while (dict.size() < deltaStart) {
1482-
dict.add(null);
1483-
}
1484-
for (int i = 0; i < deltaCount; i++) {
1485-
int len = readVarint(frame, pos);
1486-
String sym = new String(frame, pos[0], len, StandardCharsets.UTF_8);
1487-
pos[0] += len;
1488-
int idx = deltaStart + i;
1489-
while (dict.size() <= idx) {
1490-
dict.add(null);
1491-
}
1492-
dict.set(idx, sym);
1493-
}
1494-
}
14951437
}
14961438

14971439
private static class SilentHandler implements TestWebSocketServer.WebSocketServerHandler {
@@ -1510,20 +1452,11 @@ private static class CountingHandler implements TestWebSocketServer.WebSocketSer
15101452
public void onBinaryMessage(TestWebSocketServer.ClientHandler client, byte[] data) {
15111453
frames.incrementAndGet();
15121454
try {
1513-
client.sendBinary(buildAck(nextSeq.getAndIncrement()));
1455+
client.sendBinary(QwpWireTestUtils.buildAck(nextSeq.getAndIncrement()));
15141456
} catch (IOException e) {
15151457
throw new RuntimeException(e);
15161458
}
15171459
}
1518-
1519-
private static byte[] buildAck(long seq) {
1520-
byte[] buf = new byte[1 + 8 + 2];
1521-
ByteBuffer bb = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN);
1522-
bb.put((byte) 0x00);
1523-
bb.putLong(seq);
1524-
bb.putShort((short) 0);
1525-
return buf;
1526-
}
15271460
}
15281461
/**
15291462
* Short-writes the FIRST dictionary append after it is armed, modelling a disk that

0 commit comments

Comments
 (0)