chore(qwp): read the SF recovery scan via pread instead of the mapping#71
chore(qwp): read the SF recovery scan via pread instead of the mapping#71mtopolnik wants to merge 5 commits into
Conversation
Recovery-time reads of a segment file go through pread into a private buffer: the header block, the frame scan, the CRC fold, and the torn-tail probe. Unreadable regions are then ordinary read results with one deterministic outcome per input on every filesystem, JDK, and JIT state: a sparse hole reads back as zeros and fails the frame CRC, a region past real end-of-file gives a short read, and a media error gives a failed read -- each is the boundary of recoverable data. The mapping is created only after the scan, for the append path, whose writes materialize pages rather than faulting. Reading the same regions through the mapping raises SIGBUS, which HotSpot converts to an InternalError delivered at an imprecise point under a JIT-compiled caller -- not reliably catchable by any try/catch placement, which is the MmapSegmentRecoveryFaultTest flake on the JDK 8 CI (#69). With no fault to catch, the catchable-InternalError machinery (isMmapAccessFault, the table-CRC fallback, the per-method catch arms) is gone, and the frame CRC uses the native Crc32c again -- safe on a process-private buffer. The scan is one pass through a sliding 1 MiB pread window (header fields, lastGood, frame count, torn-tail probe together); frames larger than the window CRC-fold across refills. The recovery tests become single-outcome and gain a bad-sector case via the facade's read seam (renamed MmapSegmentRecoveryBoundaryTest -- there is no fault delivery left to branch on). Fixes #69. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
openExisting creates the RW mapping only after the recovery scan completes. The catch block used to close the fd but not the mapping, so a throw between a successful Files.mmap and the constructor return (realistically only an OOME allocating the MmapSegment) leaked the mapping. Track the address across the try block and munmap it in the catch before closing the fd, mirroring the cleanup pattern create() uses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RecoveryScan's constructor takes four same-typed longs, so its parameter order now matches the alphabetical field declaration order (baseSeq, frameCount, lastGood, tornTailBytes) to remove the positional-swap hazard; scanFile's call site follows. The nested classes also swap places so PreadWindow precedes RecoveryScan, keeping members alphabetically sorted per the repo convention. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The recovery boundary suite only covered files that fit in a single pread window, leaving the scan's window repositioning and the chunk-by-chunk CRC verification untested. The new test recovers a 4 MiB segment holding a 3 MiB frame followed by a small one: checking the large frame reloads the window four times, feeding each chunk into the running CRC, and the small frame is then verified out of a repositioned window. Recovery must be total -- both frames kept, the cursor at the last appended byte, no torn tail. A mutation check confirmed the test has teeth: reseeding the CRC on each window reload fails exactly this test while the rest of the suite stays green. writeSegment gains an explicit-segment-size variant to build the oversized file. The comments in MmapSegment and the test suite also drop the "fold the CRC" phrasing: the scan computes the CRC incrementally, feeding each window-sized chunk into the running value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code Review — PR #71Level 2 · 4 review agents (correctness, perf+resource, concurrency+quality+metadata, tests) + my own adversarial pass. No binary artifacts. Issue #69 confirmed OPEN (real JDK 8 CI flake). 9 draft findings verified, 2 dropped as non-defects. This is a well-engineered change. The core rewrite — scanning the CriticalNone. Moderate1. Untested code path: unreadable region partway through a large frame's CRC span ( Minor4. Multi-window test javadoc imprecision. — (Optional, not filed: the PR title describes the internal mechanism rather than user impact — borderline and acceptable given QWP is beta/internal.) Downgraded (dropped after verification)
SummaryVerdict: approve — merge after adding labels; the coverage gap (finding #1) is worth closing but need not block.
|
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>
[PR Coverage check]😍 pass : 79 / 82 (96.34%) file detail
|
|
Superseded by #67 — closing. This PR's change (reading the SF recovery scan through The two are parallel solutions to the same problem:
All six files this branch touches fall entirely inside #67's rewrite, so One behavioral difference, for the record: on an unreadable region mid-scan (a |
Fixes #69 — the
MmapSegmentRecoveryFaultTestInternalErrorflake on the JDK 8 CI — by removing the fault instead of trying to catch it better.Why catching cannot be made reliable. On pre-21 HotSpot, a SIGBUS from an
Unsaferead of an unbacked mapped page in compiled code is not thrown at the faulting instruction: the signal handler resumes execution and theInternalError("a fault occurred in a recent unsafe memory access operation…") is raised later, at the next safepoint transition. Once C2 inlines caller →openExisting→scanFramesinto one compiled unit, the delivery point can land past every lexicaltry— which is why #69's stack trace shows the error attributed to the test's line with noopenExistingframe, and why the same-framecatch (InternalError)pattern inscanFramesescaped too (both tests failed in the first CI run). #69's suggested mitigations (non-inlinable helper,SegmentRing-level catch) narrow the window but can't close it, and none of it is verifiable off JDK 8.The fix. Every recovery-time read — header block, frame scan, frame CRC checks, torn-tail probe — now goes through
preadinto a private buffer, in one pass over a sliding 1 MiB window. Unreadable regions become ordinary read results with one deterministic outcome per input on every filesystem, JDK, and JIT state:The mapping is created only after the scan, for the append path (whose writes materialize pages rather than faulting). With no fault left to catch,
isMmapAccessFault, the table-driven CRC fallback, and the per-methodcatch (InternalError)arms are deleted, and the frame CRC goes back through the nativeCrc32c— safe on a process-private buffer; a frame larger than the read window is checksummed incrementally, each window-sized chunk feeding the running CRC, producing the same valuetryAppendwrites in one pass. The production diff nets out to −16 lines (227 insertions, 243 deletions acrossMmapSegment,SegmentRing,FilesFacade).Tests. The recovery suite is renamed
MmapSegmentRecoveryBoundaryTest— there is no fault delivery left to branch on, so the former two/three-outcome tests each assert a single deterministic result, and the suite gains a bad-sector case driven through the facade'sreadseam. It also gains a multi-window case — a 4 MiB segment whose 3 MiB frame makes the scan reload the window four times and checksum it chunk by chunk, with a second frame verified out of the window the last chunk loaded; every earlier case fit inside one window, so the window-sliding machinery was otherwise untested. A deliberate mutation that reseeds the CRC on each window reload fails exactly that test and nothing else. A further case fails the read partway through the large frame's CRC span — the one path where the incremental CRC meets an unreadable region; the other unreadable-region cases all fail on a frame-header read — and asserts recovery discards the partially checksummed frame whole and skips the torn-tail probe; a mutation that drops the unreadable flag there fails exactly that test. TheFilesFacade.length(String)seam doc is updated to the pread semantics.Review follow-ups on the branch.
openExistingunmaps the mapping when a failure lands between a successfulmmapand the constructor return, instead of only closing the fd;RecoveryScan's constructor parameters follow the field declaration order and the nested helper classes sort alphabetically.Validation. Full core suite green locally (2541 tests, 0 failures) on JDK 25 — and unlike the fault-delivery behavior this replaces, the pread semantics carry no JDK sensitivity, so local validation is meaningful; the JDK 8 CI job confirms on the shipping floor.
Out of scope, noted for completeness: post-recovery mapped reads (e.g.
findLastFrameFsnWithoutPayloadFlag) touch only CRC-verified regions whose pages the scan just read, and the append path's SIGBUS-on-ENOSPC story is unchanged from #64.🤖 Generated with Claude Code