fix(#4928,#4930,#4931,#4932,#4955): storage/engine robustness bundle#5012
fix(#4928,#4930,#4931,#4932,#4955): storage/engine robustness bundle#5012lvca wants to merge 1 commit into
Conversation
#4928: a plain IOException from flushPage escaped the per-page loop and aborted the batch: the remaining pages were never flushed or retried, yet their pageIndex entries survived, so waitAllPagesOfDatabaseAreFlushed spun forever and close()/rename()/backup-suspend hung. The failure is now contained per page with the same policy as the #4965 interrupt double-fault (SEVERE log; the page's WAL entry was never acked, so it is recovered on the next open) and the batch continues. waitAllPagesOfDatabaseAreFlushed is bounded (60s) with a SEVERE escalation instead of an infinite spin. #4930: the ClosedChannelException reopen path could not tell 'closed by interrupt' from 'closed/dropped on purpose': a page mid-flight in the flush thread racing a DDL drop made the reopen RE-CREATE the deleted file (open() uses RandomAccessFile 'rw'), leaving a one-page ghost that FileManager re-registered on the next open. The reopen now refuses when the component was closed intentionally (open=false) or the OS file no longer exists, surfacing the condition to the caller. #4931: check() computed RIDs with int*int arithmetic that overflows for buckets beyond 2^31 positions (a 64GB bucket at 64KB pages); with fix=true the wrong-but-positive RID deleted an innocent record. Widened to long as scan() already does; the two log sites with the same overflow (plus a string-concat-instead-of-add bug) fixed too. #4932: deleteRecordInternal deliberately throws ConcurrentModificationException as a retry signal for concurrently-modified multi-page chunk chains, then swallowed it 60 lines later in its own generic catch: the slot pointer was zeroed, the delete reported success, the retry never happened and the remaining NEXT_CHUNK records were orphaned (permanent space leak). The retry signal is now rethrown, mirroring the existing RecordNotFoundException clause. #4955: getBestSlot/getRandomSlot snapshotted executorThreads without a null check, so scheduling async work concurrently with close()/kill() threw a raw NPE instead of the intended DatabaseOperationException that scheduleTask already produced for the same race. Tests: the batch-containment + no-resurrection test is red-first verified (on unfixed code the dropped file IS re-created on disk and the exception escapes); the bounded-wait test drives the timeout deterministically; the async-slot test asserts the intended exception after kill(). #4931 (needs a 64GB bucket) and #4932 (needs a mid-delete race) are trace-verified fixes mirroring existing correct code in the same file. Regression: engine + database packages green (114 classes, 562 tests).
|
Tick the box to add this pull request to the merge queue (same as
|
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
🟢 Coverage 78.26% diff coverage · -7.23% coverage variation
Metric Results Coverage variation ✅ -7.23% coverage variation Diff coverage ✅ 78.26% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (d85c4db) 134450 99979 74.36% Head commit (bb5037b) 166272 (+31822) 111621 (+11642) 67.13% (-7.23%) Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#5012) 23 18 78.26% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Code Review
This pull request introduces a robustness bundle for the storage engine, addressing several critical defects: it bounds the page flush wait time to prevent infinite hangs during database close, contains per-page I/O errors to avoid aborting flush batches, prevents the recreation of deleted files during channel reopens, fixes integer overflows in RID arithmetic for large buckets, and ensures concurrent modification retry signals are not swallowed during multi-page record deletions. Additionally, it resolves a shutdown race in the async executor that could lead to a NullPointerException. Feedback on the changes suggests marking the mutable static field WAIT_FLUSH_TIMEOUT_MS as volatile to guarantee thread visibility when modified dynamically during tests.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| * Upper bound for {@link #waitAllPagesOfDatabaseAreFlushed}: generous enough for any healthy flush backlog | ||
| * (the bounded queue drains far faster), small enough that a wedged flush cannot hang close() forever (#4928). | ||
| */ | ||
| static long WAIT_FLUSH_TIMEOUT_MS = 60_000L; // non-final and package-private for the #4928 regression test |
There was a problem hiding this comment.
Since WAIT_FLUSH_TIMEOUT_MS is a mutable static field that is modified dynamically in tests (such as in FlushRobustnessTest.java), it should be marked volatile to ensure thread visibility. Without volatile, updates to this field made by a test thread might not be immediately visible to other threads executing waitAllPagesOfDatabaseAreFlushed, which could lead to flaky test results or incorrect timeout behavior in concurrent environments.
| static long WAIT_FLUSH_TIMEOUT_MS = 60_000L; // non-final and package-private for the #4928 regression test | |
| static volatile long WAIT_FLUSH_TIMEOUT_MS = 60_000L; // non-final and package-private for the #4928 regression test |
PR Review: storage/engine robustness bundle (#4928, #4930, #4931, #4932, #4955)Thanks for this bundle - the fixes are well-scoped, the comments explain the "why" clearly, and the red-first tests are a nice touch. Most changes are clean wins. I have one substantive correctness concern on the #4928 bounded-wait, plus a few smaller notes. 🔴 Main concern: the "recovered from the WAL on next open" guarantee does not hold on the
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #5012 +/- ##
============================================
- Coverage 65.36% 65.36% -0.01%
- Complexity 815 816 +1
============================================
Files 1683 1683
Lines 134450 134469 +19
Branches 28764 28769 +5
============================================
+ Hits 87887 87891 +4
- Misses 34508 34528 +20
+ Partials 12055 12050 -5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
#4928: a plain IOException from flushPage escaped the per-page loop and aborted the batch: the remaining pages were never flushed or retried, yet their pageIndex entries survived, so waitAllPagesOfDatabaseAreFlushed spun forever and close()/rename()/backup-suspend hung. The failure is now contained per page with the same policy as the #4965 interrupt double-fault (SEVERE log; the page's WAL entry was never acked, so it is recovered on the next open) and the batch continues. waitAllPagesOfDatabaseAreFlushed is bounded (60s) with a SEVERE escalation instead of an infinite spin.
#4930: the ClosedChannelException reopen path could not tell 'closed by interrupt' from 'closed/dropped on purpose': a page mid-flight in the flush thread racing a DDL drop made the reopen RE-CREATE the deleted file (open() uses RandomAccessFile 'rw'), leaving a one-page ghost that FileManager re-registered on the next open. The reopen now refuses when the component was closed intentionally (open=false) or the OS file no longer exists, surfacing the condition to the caller.
#4931: check() computed RIDs with int*int arithmetic that overflows for buckets beyond 2^31 positions (a 64GB bucket at 64KB pages); with fix=true the wrong-but-positive RID deleted an innocent record. Widened to long as scan() already does; the two log sites with the same overflow (plus a string-concat-instead-of-add bug) fixed too.
#4932: deleteRecordInternal deliberately throws
ConcurrentModificationException as a retry signal for concurrently-modified multi-page chunk chains, then swallowed it 60 lines later in its own generic catch: the slot pointer was zeroed, the delete reported success, the retry never happened and the remaining NEXT_CHUNK records were orphaned (permanent space leak). The retry signal is now rethrown, mirroring the existing RecordNotFoundException clause.
#4955: getBestSlot/getRandomSlot snapshotted executorThreads without a null check, so scheduling async work concurrently with close()/kill() threw a raw NPE instead of the intended DatabaseOperationException that scheduleTask already produced for the same race.
Tests: the batch-containment + no-resurrection test is red-first verified (on unfixed code the dropped file IS re-created on disk and the exception escapes); the bounded-wait test drives the timeout deterministically; the async-slot test asserts the intended exception after kill(). #4931 (needs a 64GB bucket) and #4932 (needs a mid-delete race) are trace-verified fixes mirroring existing correct code in the same file. Regression: engine + database packages green (114 classes, 562 tests).
What does this PR do?
A brief description of the change being made with this pull request.
Motivation
What inspired you to submit this pull request?
Related issues
A list of issues either fixed, containing architectural discussions, otherwise relevant
for this Pull Request.
Additional Notes
Anything else we should know when reviewing?
Checklist
mvn clean packagecommand