2626
2727import io .questdb .client .cutlass .qwp .client .sf .cursor .MmapSegment ;
2828import io .questdb .client .cutlass .qwp .client .sf .cursor .MmapSegmentException ;
29+ import io .questdb .client .cutlass .qwp .client .sf .cursor .SegmentRing ;
2930import io .questdb .client .std .Files ;
3031import io .questdb .client .std .FilesFacade ;
3132import io .questdb .client .std .MemoryTag ;
3637import org .junit .Test ;
3738
3839import static org .junit .Assert .assertEquals ;
40+ import static org .junit .Assert .assertNull ;
3941import static org .junit .Assert .assertTrue ;
40- import static org .junit .Assert .fail ;
4142
4243/**
4344 * Regression guard for the recovery-time SIGBUS hazard in {@link MmapSegment}.
5253 * keep every frame below it, and hand back a usable segment -- not let the
5354 * error abort recovery of the whole slot (the reported ZFS-CI flake).
5455 * <p>
55- * These tests drive the <b>production entry point</b> ({@code openExisting}),
56- * not the private scan methods via reflection. That matters for two reasons:
56+ * These tests drive the <b>production entry points</b> -- the two
57+ * header-skippable cases go through the outermost one,
58+ * {@code SegmentRing.openExisting} (production's real recovery caller, whose
59+ * per-file catch does the skip), and the rest through
60+ * {@code MmapSegment.openExisting} -- not the private scan methods via
61+ * reflection. That matters for two reasons:
5762 * <ul>
5863 * <li>It exercises the real recovery path end to end.</li>
5964 * <li>On pre-21 JDKs the mmap-fault {@code InternalError} is delivered
8691 * mmap-fault guard reverted -- no regression protection on the ext4/xfs CI
8792 * runners. The two {@code MapPastEof} tests below close that gap portably.
8893 * They truncate the file <em>down</em> (freeing the tail blocks) and hand
89- * {@code openExisting} a {@link FilesFacade} that reports the original, larger
94+ * recovery a {@link FilesFacade} that reports the original, larger
9095 * length, so the mapping extends past real end-of-file. A read of a page beyond
9196 * real EOF raises SIGBUS on <em>every</em> filesystem -- the same catchable
9297 * {@code InternalError} an unbacked ZFS page raises -- so they exercise the
@@ -181,7 +186,7 @@ public void testRecoverySurvivesPayloadReachingUnbackedPage() throws Exception {
181186 * M1 regression: the header block (magic/version/baseSeq) is read before
182187 * {@code scanFrames}, so an unbacked page 0 faults ahead of the guarded
183188 * scan. {@link MmapSegment#openExisting} must surface that as a
184- * {@link MmapSegmentException} -- the per-file signal {@code SegmentRing}
189+ * {@link MmapSegmentException} -- the per-file signal {@link SegmentRing}
185190 * catches to skip just this {@code .sfa} -- and never let the raw
186191 * {@code InternalError} escape and abort recovery of every sibling segment.
187192 * <p>
@@ -190,6 +195,15 @@ public void testRecoverySurvivesPayloadReachingUnbackedPage() throws Exception {
190195 * catch; on a hole-zero-filling FS (ext4) page 0 reads back as zeroes, so
191196 * the magic check fails and throws {@code MmapSegmentException} directly.
192197 * Either way the file is skippable, not fatal.
198+ * <p>
199+ * Driven through {@link SegmentRing#openExisting} -- the real recovery path
200+ * that performs the per-file skip -- so the assertion is exactly the
201+ * production outcome: the sole unreadable segment is skipped and recovery
202+ * yields no ring. Going through {@code SegmentRing} also keeps the faulting
203+ * header read behind a real (non-inlined) call frame, so that on ZFS
204+ * HotSpot's C2 cannot deliver the async unsafe-access fault past
205+ * {@code openExisting}'s catch the way a direct call can -- see
206+ * {@link #testHeaderFaultOnMapPastEofIsSkippableAnyFilesystem}.
193207 */
194208 @ Test
195209 public void testUnbackedHeaderPageIsSkippableNotFatal () throws Exception {
@@ -198,13 +212,10 @@ public void testUnbackedHeaderPageIsSkippableNotFatal() throws Exception {
198212 writeSegment (path , 3L , new int []{64 });
199213 // Punch the whole file into a hole -- page 0 (the header) included.
200214 punchSparseTail (path , 0L );
201- try {
202- MmapSegment .openExisting (path ).close ();
203- fail ("expected MmapSegmentException for an unbacked header page" );
204- } catch (MmapSegmentException expected ) {
205- // ok -- SegmentRing's per-file catch skips just this file
206- // instead of aborting recovery of the whole slot.
207- }
215+ // The sole .sfa is unreadable (a faulting or zeroed header page), so
216+ // SegmentRing skips it and recovery produces no ring at all.
217+ assertNull ("an unbacked header page must be skipped, not recovered or fatal" ,
218+ SegmentRing .openExisting (tmpDir , SEGMENT_BYTES ));
208219 });
209220 }
210221
@@ -270,13 +281,30 @@ public void testScanFaultOnMapPastEofIsHandledAnyFilesystem() throws Exception {
270281
271282 /**
272283 * Portable fail-on-revert guard for the {@code openExisting} header-block
273- * guard. The file is truncated to empty and the facade reports a full page,
274- * so the very first header read (magic) lands on a beyond-EOF page and
275- * faults on any filesystem. {@code openExisting} must convert that to a
284+ * guard, driven through the real recovery entry point
285+ * {@link SegmentRing#openExisting}. The file is truncated to empty and the
286+ * facade reports a full page, so the very first header read (magic) lands on
287+ * a beyond-EOF page and faults on any filesystem.
288+ * {@link MmapSegment#openExisting} must convert that to a
276289 * {@link MmapSegmentException} -- the per-file signal {@code SegmentRing}
277- * skips on -- not let the raw {@code InternalError} escape and abort recovery
278- * of every sibling. Revert the header-block conversion and this throws
279- * {@code InternalError} instead of {@code MmapSegmentException}.
290+ * catches to skip just this {@code .sfa} -- so recovery of the sole
291+ * (unreadable) segment yields no ring rather than aborting or surfacing a
292+ * raw {@code InternalError}. Revert the header-block conversion and the raw
293+ * {@code InternalError} escapes {@code openExisting}, propagates out of
294+ * {@code SegmentRing}'s recovery loop, and this errors instead of returning
295+ * {@code null}.
296+ * <p>
297+ * Routing through {@code SegmentRing} rather than calling
298+ * {@code MmapSegment.openExisting} directly is deliberate, and is what makes
299+ * this test stable on the JDK 8 CI. It mirrors production, where the
300+ * faulting header read sits behind a real (non-inlined) call frame whose
301+ * {@code catch (Throwable)} runs the fault-to-{@code MmapSegmentException}
302+ * conversion. A direct call instead lets HotSpot's C2 inline
303+ * {@code openExisting} into the handler-less test frame and deliver the
304+ * async "recent unsafe memory access" {@code InternalError} past that
305+ * inlined catch -- an artifact of the test call shape, not of any
306+ * production path -- which flaked the direct-call form on the larger
307+ * (JIT-warming) CI suite.
280308 */
281309 @ Test
282310 public void testHeaderFaultOnMapPastEofIsSkippableAnyFilesystem () throws Exception {
@@ -285,16 +313,15 @@ public void testHeaderFaultOnMapPastEofIsSkippableAnyFilesystem() throws Excepti
285313 final long page = Files .PAGE_SIZE ;
286314 writeSegment (path , 9L , new int []{64 });
287315 // Free every block: the file is now empty, so even page 0 (the
288- // header) is beyond EOF under the reported one-page mapping.
316+ // header) is beyond EOF under the reported one-page mapping. The
317+ // facade reports a full page, so openExisting maps that beyond-EOF
318+ // page and the header read faults on it on any filesystem.
289319 truncateTo (path , 0L );
290320 FilesFacade ff = new MapPastEofFacade (path , page );
291- try {
292- MmapSegment .openExisting (ff , path ).close ();
293- fail ("expected MmapSegmentException for a beyond-EOF header page" );
294- } catch (MmapSegmentException expected ) {
295- // ok -- SegmentRing's per-file catch skips just this file
296- // instead of aborting recovery of the whole slot.
297- }
321+ // SegmentRing opens the sole segment through the facade, catches the
322+ // converted MmapSegmentException, skips the file, and returns no ring.
323+ assertNull ("a beyond-EOF header page must be skipped, not recovered or fatal" ,
324+ SegmentRing .openExisting (ff , tmpDir , SEGMENT_BYTES ));
298325 });
299326 }
300327
0 commit comments