Skip to content

Commit 27aad9e

Browse files
glasstigerclaude
andcommitted
Cover the split-batch delta contract
flushPendingRowsSplit fires when one flush's encoded size exceeds the server's batch cap: it emits one frame per table. The first frame must carry the whole batch's symbol-dict delta and advance the baseline, and the remaining frames must carry an empty delta that only references ids the first frame already registered -- otherwise a fresh server would see dangling symbol ids. No test drove that producer-side split. Add a test that buffers two padded tables into one flush under a small advertised cap, so the batch splits, and asserts the first frame ships deltaStart=0/deltaCount=2 while the second ships deltaStart=2/deltaCount=0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d89f14f commit 27aad9e

1 file changed

Lines changed: 46 additions & 0 deletions

File tree

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,52 @@ public void testMemoryModeShipsMonotonicDelta() throws Exception {
227227
});
228228
}
229229

230+
@Test
231+
public void testSplitBatchShipsDeltaOnFirstFrameOnly() throws Exception {
232+
// A single flush whose encoded size exceeds the server's batch cap is
233+
// split into one frame per table (flushPendingRowsSplit). The FIRST split
234+
// frame carries the whole batch's symbol-dict delta and advances the
235+
// baseline; the remaining frames carry an EMPTY delta and only reference
236+
// ids the first frame already registered. A regression that shipped each
237+
// table's own symbols (wrong deltaStart) would dangle ids on the server.
238+
assertMemoryLeak(() -> {
239+
CapturingHandler handler = new CapturingHandler();
240+
try (TestWebSocketServer server = new TestWebSocketServer(handler)) {
241+
server.setAdvertisedMaxBatchSize(150); // forces the two-table batch to split
242+
int port = server.getPort();
243+
server.start();
244+
Assert.assertTrue(server.awaitStart(5, TimeUnit.SECONDS));
245+
246+
// Padding inflates each table past half the cap, so the combined
247+
// two-table message exceeds it while each single-table frame fits.
248+
String pad = new String(new char[60]).replace('\0', 'x');
249+
try (Sender sender = Sender.fromConfig("ws::addr=localhost:" + port + ";")) {
250+
// Buffer TWO tables (symbols "a" id 0, "b" id 1), then ONE flush.
251+
sender.table("t1").symbol("s", "a").stringColumn("p", pad).longColumn("v", 1L).atNow();
252+
sender.table("t2").symbol("s", "b").stringColumn("p", pad).longColumn("v", 2L).atNow();
253+
sender.flush();
254+
waitFor(() -> handler.batches.size() >= 2, 5_000);
255+
}
256+
257+
Assert.assertEquals("the oversized two-table batch must split into 2 frames",
258+
2, handler.batches.size());
259+
byte[] f1 = handler.batches.get(0);
260+
byte[] f2 = handler.batches.get(1);
261+
262+
// First split frame ships the whole batch's dictionary (a + b).
263+
Assert.assertEquals("first split frame deltaStart must be 0",
264+
0, readVarint(f1, DELTA_START_OFFSET));
265+
Assert.assertEquals("first split frame ships both new symbols",
266+
2, readVarint(f1, DELTA_START_OFFSET + 1));
267+
// Second split frame carries an empty delta above the advanced baseline.
268+
Assert.assertEquals("second split frame deltaStart must be 2 (baseline advanced)",
269+
2, readVarint(f2, DELTA_START_OFFSET));
270+
Assert.assertEquals("second split frame carries no new symbols",
271+
0, readVarint(f2, DELTA_START_OFFSET + 1));
272+
}
273+
});
274+
}
275+
230276
private static int readVarint(byte[] buf, int offset) {
231277
// Simple unsigned varint decode — sufficient for small values.
232278
int result = 0;

0 commit comments

Comments
 (0)