Skip to content

Commit a0806f5

Browse files
mtopolnikclaude
andcommitted
Cover read failure inside a frame's CRC span
testReadErrorMidFrameCrcSpanEndsRecoveryAtFrameStart covers the one recovery path the suite left unexercised: a read failure partway through a large frame's CRC span. Every earlier unreadable-region test fails on a frame-header read, taking the scan loop's outer break; this test makes the failure land on the second CRC chunk of a 3 MiB frame, driving the labeled break inside scanFile's incremental CRC loop. Recovery must discard the partially checksummed frame whole, keep the intact frame below it, and skip the torn-tail probe. A mutation check confirms the test discriminates: dropping the unreadable flag on that branch makes the torn-tail probe run against the unprobeable region and misreport a ~4 MiB torn tail, failing exactly this test and no other. The multi-window test's javadoc claimed the small second frame is verified out of a repositioned window; the window the large frame's final CRC chunk loaded already covers it, and the javadoc now says so. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 1cb4c65 commit a0806f5

1 file changed

Lines changed: 42 additions & 3 deletions

File tree

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

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,10 @@ public void testReadErrorDuringScanEndsRecoveryAtBoundary() throws Exception {
273273
* incrementally -- reloading the window several times and feeding each
274274
* chunk into the running value, which chained {@code Crc32c.update} calls
275275
* make bit-identical to {@code tryAppend}'s one-pass CRC -- and the small
276-
* frame is then verified out of a repositioned window. Every byte is
277-
* readable, so recovery must be total: both frames kept, the cursor at
278-
* the last appended byte, no torn tail.
276+
* frame is then verified out of the window the large frame's final CRC
277+
* chunk loaded, a window positioned near the file's end rather than at
278+
* offset 0. Every byte is readable, so recovery must be total: both
279+
* frames kept, the cursor at the last appended byte, no torn tail.
279280
*/
280281
@Test
281282
public void testScanRecoversFrameLargerThanReadWindow() throws Exception {
@@ -299,6 +300,44 @@ public void testScanRecoversFrameLargerThanReadWindow() throws Exception {
299300
});
300301
}
301302

303+
/**
304+
* A failing read partway through a large frame's CRC span: the frame's
305+
* header preads fine and the first window-sized CRC chunk succeeds, but
306+
* the next chunk hits the unreadable region. A partially checksummed
307+
* frame is unverifiable, so recovery must discard it whole -- stop at
308+
* its start, keep the intact frame below it, and report no torn tail
309+
* (the region cannot be probed). This is the one path where the
310+
* incremental multi-window CRC meets an unreadable region; the other
311+
* unreadable-region tests all fail on a frame-header read instead.
312+
*/
313+
@Test
314+
public void testReadErrorMidFrameCrcSpanEndsRecoveryAtFrameStart() throws Exception {
315+
TestUtils.assertMemoryLeak(() -> {
316+
final String path = tmpDir + "/seg-read-error-mid-frame.sfa";
317+
// A small intact frame, then a 3 MiB frame whose CRC span takes
318+
// several window loads (sized against
319+
// MmapSegment.RECOVERY_BUF_BYTES = 1 MiB).
320+
final int smallLen = 64;
321+
final int largeLen = 3 * (1 << 20);
322+
final long segmentBytes = 4L * (1 << 20);
323+
writeSegment(path, 21L, new int[]{smallLen, largeLen}, segmentBytes);
324+
final long largeFrameOffset = MmapSegment.HEADER_SIZE + MmapSegment.FRAME_HEADER_SIZE + smallLen;
325+
// Fail reads from 2 MiB on: past the large frame's header and its
326+
// entire first CRC chunk, but inside its payload -- the failure
327+
// lands on the second chunk's read, in the CRC loop, not on a
328+
// frame-header read.
329+
FilesFacade ff = new RecoverySeamFacade(path, -1L, 2L * (1 << 20));
330+
try (MmapSegment seg = MmapSegment.openExisting(ff, path)) {
331+
assertEquals("the intact frame below the unreadable region must be recovered",
332+
1L, seg.frameCount());
333+
assertEquals("recovery must stop at the start of the partially checksummed frame",
334+
largeFrameOffset, seg.publishedOffset());
335+
assertEquals("an unreadable region cannot be probed for a torn write",
336+
0L, seg.tornTailBytes());
337+
}
338+
});
339+
}
340+
302341
/**
303342
* Creates a segment at {@code path} and appends one frame per entry in
304343
* {@code payloadLens} (each filled with non-zero bytes so recovery can tell

0 commit comments

Comments
 (0)