diff --git a/core/src/main/java/io/questdb/client/cutlass/qwp/client/sf/cursor/SegmentRing.java b/core/src/main/java/io/questdb/client/cutlass/qwp/client/sf/cursor/SegmentRing.java index c8516c4c..6bada043 100644 --- a/core/src/main/java/io/questdb/client/cutlass/qwp/client/sf/cursor/SegmentRing.java +++ b/core/src/main/java/io/questdb/client/cutlass/qwp/client/sf/cursor/SegmentRing.java @@ -25,6 +25,7 @@ package io.questdb.client.cutlass.qwp.client.sf.cursor; import io.questdb.client.std.Files; +import io.questdb.client.std.FilesFacade; import io.questdb.client.std.ObjList; import io.questdb.client.std.QuietCloseable; import org.jetbrains.annotations.TestOnly; @@ -155,6 +156,27 @@ public SegmentRing(MmapSegment initialActive, long maxBytesPerSegment) { * depends on every segment being readable. */ public static SegmentRing openExisting(String sfDir, long maxBytesPerSegment) { + return openExisting(FilesFacade.INSTANCE, sfDir, maxBytesPerSegment); + } + + /** + * Facade-aware variant of {@link #openExisting(String, long)} that opens + * each recovered segment through the given {@link FilesFacade} -- via + * {@link MmapSegment#openExisting(FilesFacade, String)} -- instead of + * {@link FilesFacade#INSTANCE}. Production calls the {@code INSTANCE} + * overload above; this seam exists so recovery's mmap-fault handling can be + * regression-tested end to end on any filesystem. A facade that reports an + * inflated length maps a segment past real end-of-file, so the recovery + * read faults deterministically -- the same catchable {@code InternalError} + * an unbacked/sparse page raises on ZFS, but reproduced on ext4/xfs where a + * within-EOF hole would instead zero-fill and never exercise the guard. + *

+ * Only the per-segment open is routed through {@code ff}; directory + * enumeration and the empty-orphan unlink/quarantine stay on {@link Files}, + * mirroring {@code MmapSegment.openExisting}'s own partial seam (which keeps + * {@code mmap} on {@code Files}). + */ + public static SegmentRing openExisting(FilesFacade ff, String sfDir, long maxBytesPerSegment) { if (!Files.exists(sfDir)) { return null; } @@ -182,7 +204,7 @@ public static SegmentRing openExisting(String sfDir, long maxBytesPerSegment) { String path = sfDir + "/" + name; MmapSegment seg = null; try { - seg = MmapSegment.openExisting(path); + seg = MmapSegment.openExisting(ff, path); // Filter out empty leftovers -- typically hot-spare // segments the manager pre-allocated for a prior // session that never got rotated into active. They diff --git a/core/src/test/java/io/questdb/client/test/cutlass/qwp/client/sf/cursor/MmapSegmentRecoveryFaultTest.java b/core/src/test/java/io/questdb/client/test/cutlass/qwp/client/sf/cursor/MmapSegmentRecoveryFaultTest.java index 861f761c..d2ea21c7 100644 --- a/core/src/test/java/io/questdb/client/test/cutlass/qwp/client/sf/cursor/MmapSegmentRecoveryFaultTest.java +++ b/core/src/test/java/io/questdb/client/test/cutlass/qwp/client/sf/cursor/MmapSegmentRecoveryFaultTest.java @@ -26,6 +26,7 @@ import io.questdb.client.cutlass.qwp.client.sf.cursor.MmapSegment; import io.questdb.client.cutlass.qwp.client.sf.cursor.MmapSegmentException; +import io.questdb.client.cutlass.qwp.client.sf.cursor.SegmentRing; import io.questdb.client.std.Files; import io.questdb.client.std.FilesFacade; import io.questdb.client.std.MemoryTag; @@ -36,8 +37,8 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; /** * Regression guard for the recovery-time SIGBUS hazard in {@link MmapSegment}. @@ -52,8 +53,12 @@ * keep every frame below it, and hand back a usable segment -- not let the * error abort recovery of the whole slot (the reported ZFS-CI flake). *

- * These tests drive the production entry point ({@code openExisting}), - * not the private scan methods via reflection. That matters for two reasons: + * These tests drive the production entry points -- the two + * header-skippable cases go through the outermost one, + * {@code SegmentRing.openExisting} (production's real recovery caller, whose + * per-file catch does the skip), and the rest through + * {@code MmapSegment.openExisting} -- not the private scan methods via + * reflection. That matters for two reasons: *