fix(#4319): MutablePage.move mis-tracked WAL range for backward shifts#4325
Conversation
For backward shifts (destPosition < startPosition), updateModifiedRange was called with startPosition as the lower bound instead of destPosition, leaving [destPosition, startPosition) untracked in the WAL delta. A crash after defragPage would replay WAL without those bytes, corrupting the bucket page. Fix: use Math.min(startPosition, destPosition) as the lower bound so backward shifts start tracking at destPosition. Upper bound stays destPosition + length which is always within page bounds. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Coverage variation | ✅ -7.36% coverage variation |
| Diff coverage | ✅ 100.00% diff coverage |
Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (d1b1632) 127537 93311 73.16% Head commit (aa361a3) 159216 (+31679) 104777 (+11466) 65.81% (-7.36%) 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 (#4325) 2 2 100.00% 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 addresses issue #4319 by fixing a bug in MutablePage.move where backward shifts were incorrectly tracked in the Write-Ahead Log (WAL), potentially leading to page corruption upon recovery. The fix ensures the modified range lower bound is the minimum of the source and destination positions. Review feedback correctly identified an off-by-one error in the upper bound calculation of updateModifiedRange, which could trigger an IllegalArgumentException when data is moved to the end of a page. The reviewer also suggested ensuring the range is only updated for moves with a length greater than zero and adding a specific test case for boundary conditions at the end of the page.
| page.move(50, 50, 60); | ||
| final int[] range = page.getModifiedRange(); | ||
| assertThat(range[0]).isEqualTo(50 + BasePage.PAGE_HEADER_SIZE); | ||
| } |
There was a problem hiding this comment.
It would be beneficial to add a test case for moving data to the very end of the page to ensure that the inclusive boundary logic is correctly handled and doesn't trigger out-of-bounds exceptions.
}
@Test
void moveToEndOfPage() {
final MutablePage page = freshPage();
// PAGE_SIZE=1024, HEADER=8. Max content length is 1016.
// Move 10 bytes to the very end of the page (dest 1006 + length 10 = 1016 relative to header).
page.move(0, 1006, 10);
final int[] range = page.getModifiedRange();
assertThat(range[1]).isEqualTo(1023);
}|
Code review for PR 4325 - MutablePage.move WAL range fix. The one-line fix (Math.min for lower bound) is correct and the root cause analysis is accurate. Four items before merging: (1) Remove docs/4319-mutablepage-move-wal-tracking.md - it is an analysis doc that belongs only in the PR description, not the source tree. (2) Pre-existing: move() passes destPosition+length as exclusive end to updateModifiedRange, but the convention elsewhere (checkBoundariesOnWrite line 235) is inclusive (start+length-1). Side effects: one extra byte over-tracked in WAL (harmless), and a potential bounds-check throw if bytes are moved to the very last position in the page. Not introduced by this PR but worth a follow-up fix changing the upper bound to destPosition+length-1. (3) Comment in forwardShiftModifiedRangeStartsAtSource is ambiguous - it says 'written bytes start at destPosition but range from source' which could mislead a reader into thinking the range should start at destPosition. Suggest: 'range starts at min(start,dest)=start for forward shifts; verifies the fix does not regress forward-shift behaviour.' (4) Upper bound assertion in backwardShiftModifiedRangeCoversWrittenBytes is loose - it only checks range[1] >= destPosition+length-1 with no upper cap, so it would pass even if modifiedRangeTo were MAX_VALUE. Adding range[1] <= destPosition+length would tighten it. What is good: backwardShiftModifiedRangeStartsAtDest directly catches the regression (old code gives range[0]=108, fix gives 58), Mockito is consistent with SQLMethodSizeTest, tests are in the right package for package-private access, no new dependencies. |
…lePage.move - Use Math.max(start, dest) + length - 1 (inclusive) for updateModifiedRange upper bound, consistent with all other call sites - Add length > 0 guard to prevent inverted range when length is zero - Add moveToEndOfPageDoesNotThrow test for boundary regression Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Code Review - PR #4325: fix(#4319): MutablePage.move mis-tracked WAL range for backward shiftsOverviewThis PR fixes a data-corruption bug in Core Fix (
|
| Category | Assessment |
|---|---|
| Bug fix correctness | Correct |
| Off-by-one cleanup | Correct bonus fix |
| Test coverage | Good, minor gaps noted |
| Style/conventions | Consistent with codebase |
| Docs file | Should be removed (transient analysis doc) |
The core code change is solid. The main ask before merging is dropping the docs/4319-*.md file, and optionally adding a length == 0 test and fixing the misleading comment in forwardShiftModifiedRangeStartsAtSource.
Code ReviewThe bug fix itself is correct and important - a crash after Fix AnalysisThe one-liner change in if (length > 0)
updateModifiedRange(Math.min(startPosition, destPosition), Math.max(startPosition, destPosition) + length - 1);
The range is slightly conservative - for non-overlapping moves it includes the source region in addition to the written region. That is safe for WAL (recovery replays the wider slice, all bytes are consistent) and the simplicity of Issues to Address1. Remove
|
| Bug fix correctness | Correct |
| Off-by-one fix (inclusive upper bound) | Correct, good catch |
length == 0 guard |
Correct |
| Test coverage | Good - backward, forward, same-position, boundary cases all covered |
| Blocking issue | docs/4319-mutablepage-move-wal-tracking.md should not be committed |
| Minor issues | Strengthen loose assertions; clarify one test comment |
The fix is ready to merge once the documentation file is removed and the test assertions tightened.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4325 +/- ##
=========================================
Coverage 64.11% 64.11%
+ Complexity 423 419 -4
=========================================
Files 1645 1645
Lines 127537 127538 +1
Branches 27348 27349 +1
=========================================
+ Hits 81766 81775 +9
- Misses 34257 34261 +4
+ Partials 11514 11502 -12 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Closes #4319
Summary
MutablePage.move()calledupdateModifiedRange(startPosition, destPosition + length). For backward shifts (destPosition < startPosition— theLocalBucket.defragPagecase), the lower bound wasstartPositioninstead ofdestPosition, leaving the range[destPosition, startPosition)untracked in the WAL delta. A crash afterdefragPagewould replay WAL without those bytes, silently corrupting the bucket page.Fix: use
Math.min(startPosition, destPosition)as the lower bound. The upper bound staysdestPosition + length, which is always within page bounds (validated byBinary.movebeforeupdateModifiedRangeis reached). The issue's suggestedMath.max(start, dest) + lengthvariant was not used because for a backward shift it can equal the physical page size and triggerupdateModifiedRange's bounds check.Test plan
MutablePageMoveTestunit tests verify correctgetModifiedRange()after backward shift, forward shift, same-position shiftRecordRecyclingTest(which exercisesdefragPagevia record deletion and recycling) continues to pass🤖 Generated with Claude Code