Skip to content

Commit 0c0b9dc

Browse files
glasstigerclaude
andcommitted
Cover file-mode split write-ahead dict persist
flushPendingRowsSplit's write-ahead dictionary persist (the appendRawEntries fast path) runs only in the exceptional over-cap split path, and both split tests were memory-mode, so the split-times-persist path was never exercised with a live PersistedSymbolDict -- a recovered slot relies on it to rebuild the delta frames. Add testFileModeSplitPersistsDictBeforePublish: a file-mode two-table batch that exceeds the server cap splits into two frames, and the first frame's persist must record both new symbols in .symbol-dict. Dropping the split persist leaves the file empty and fails the test; the non-split file persist test is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 031777e commit 0c0b9dc

1 file changed

Lines changed: 49 additions & 0 deletions

File tree

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,55 @@ public void testOversizedTableSplitStrandsNothing() throws Exception {
332332
});
333333
}
334334

335+
@Test
336+
public void testFileModeSplitPersistsDictBeforePublish() throws Exception {
337+
// File-mode store-and-forward + a SPLIT flush: a two-table batch whose combined
338+
// size exceeds the server cap splits into one frame per table
339+
// (flushPendingRowsSplit). The first split frame's write-ahead persist
340+
// (persistNewSymbolsBeforePublish, the appendRawEntries fast path) must record
341+
// the batch's new symbols in .symbol-dict BEFORE the frames publish, so a
342+
// recovered / orphan-drained slot can rebuild what the delta frames reference.
343+
// The other split tests run in memory mode, so this is the only coverage of the
344+
// split x persist path with a live PersistedSymbolDict.
345+
Path sfDir = Files.createTempDirectory("qwp-sf-split-persist");
346+
try {
347+
assertMemoryLeak(() -> {
348+
CapturingHandler handler = new CapturingHandler();
349+
try (TestWebSocketServer server = new TestWebSocketServer(handler)) {
350+
server.setAdvertisedMaxBatchSize(150); // forces the two-table batch to split
351+
int port = server.getPort();
352+
server.start();
353+
Assert.assertTrue(server.awaitStart(5, TimeUnit.SECONDS));
354+
355+
String config = "ws::addr=localhost:" + port + ";sf_dir=" + sfDir + ";";
356+
Path dictFile = sfDir.resolve("default").resolve(".symbol-dict");
357+
String pad = new String(new char[60]).replace('\0', 'x');
358+
try (Sender sender = Sender.fromConfig(config)) {
359+
// Two tables, two new symbols, ONE flush -> the combined message
360+
// exceeds cap 150 and splits into two frames.
361+
sender.table("t1").symbol("s", "alpha").stringColumn("p", pad).longColumn("v", 1L).atNow();
362+
sender.table("t2").symbol("s", "bravo").stringColumn("p", pad).longColumn("v", 2L).atNow();
363+
sender.flush();
364+
waitFor(() -> handler.batches.size() >= 2, 5_000);
365+
366+
// Check .symbol-dict while the sender is live: a fully-drained
367+
// close would unlink it. The split's first-frame write-ahead
368+
// persist must have recorded BOTH new symbols.
369+
Assert.assertTrue("persisted dictionary file exists", Files.exists(dictFile));
370+
byte[] dict = Files.readAllBytes(dictFile);
371+
Assert.assertTrue("split-flush persist must record alpha", containsUtf8(dict, "alpha"));
372+
Assert.assertTrue("split-flush persist must record bravo", containsUtf8(dict, "bravo"));
373+
}
374+
375+
Assert.assertEquals("the oversized two-table batch must split into 2 frames",
376+
2, handler.batches.size());
377+
}
378+
});
379+
} finally {
380+
rmDir(sfDir);
381+
}
382+
}
383+
335384
private static int readVarint(byte[] buf, int offset) {
336385
// Simple unsigned varint decode — sufficient for small values.
337386
int result = 0;

0 commit comments

Comments
 (0)