HBASE-30335 Seed master flushedSequenceIdByRegion with openSeqNum on region OPEN - #8584
nirdosh0110 wants to merge 8 commits into
Conversation
…region OPEN When a region is opened, the master does not populate flushedSequenceIdByRegion until the hosting RegionServer's next heartbeat delivers a flush report. If the source RegionServer of a drain-move crashes before that heartbeat, ServerManager. getLastFlushedSequenceId returns NO_SEQNUM (-1) for the region, and WALSplitter conservatively writes already-durable edits into recovered.edits. Those orphaned edits then trigger false-positive "data loss" warnings during subsequent merge/split operations and leave regions stuck in RIT. Add ServerManager.reportRegionOpen(regionInfo, openSeqNum) and call it from AssignmentManager.regionOpenedWithoutPersistingToMeta so the watermark is established synchronously at OPEN time. putIfAbsent is used so a subsequent heartbeat with a higher completedSequenceId is never regressed by a stale open value. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| regionStates.removeFromFailedOpen(regionInfo); | ||
| // HBASE-30335: seed the master's flushed sequence cache with openSeqNum so a subsequent | ||
| // WAL split (e.g. source RS crashes after drain-move) recognizes already-durable edits | ||
| // instead of writing orphaned recovered.edits. |
There was a problem hiding this comment.
It would be good to add tests.
…estGetLastFlushedSequenceId New unit test TestServerManager covers reportRegionOpen behavior: - seeds flushedSequenceIdByRegion with the supplied openSeqNum; - putIfAbsent semantics prevent regressing a higher watermark that was already established (by an earlier open or a heartbeat); - NO_SEQNUM and negative openSeqNum are ignored (no-op). TestGetLastFlushedSequenceId previously asserted the pre-flush lastFlushedSequenceId was NO_SEQNUM. That assumption is invalidated by the fix (openSeqNum is now seeded synchronously on OPEN); the assertion is updated to require the watermark be present but strictly less than the memstore's earliest unflushed edit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
I think this is an optimization for not splitting unnecessary wal edits, but why it will lead to merge stuck? After opening a region, we should have removed all the recovered.edits files? |
Thanks @Apache9 for looking into it. Below is incident brief explanation. I've also raised PR8583 to reopen parent region on rollback of MERGE_TABLE_REGIONS_CHECK_CLOSED_REGIONS linked to this incident. What Happened:
Where it got stuck: At 18:32:33 UTC, MergeTableRegionsProcedure (pid=46990253) unassigned the region successfully. |
apurtell
left a comment
There was a problem hiding this comment.
Approved, with two suggestions
| if (openSeqNum == HConstants.NO_SEQNUM || openSeqNum < 0) { | ||
| return; | ||
| } | ||
| flushedSequenceIdByRegion.putIfAbsent(regionInfo.getEncodedNameAsBytes(), openSeqNum); |
There was a problem hiding this comment.
A max merge is more correct and equally safe, because at OPEN time a region cannot have flushed past its own openSeqNum.
public void reportRegionOpen(final RegionInfo regionInfo, final long openSeqNum) {
if (openSeqNum < 0) { // NO_SEQNUM == -1
return;
}
flushedSequenceIdByRegion.merge(regionInfo.getEncodedNameAsBytes(), openSeqNum, Math::max);
}There was a problem hiding this comment.
Addressed this comment in latest commit.
| // longer NO_SEQNUM before the first flush - it is the region's openSeqNum, which must | ||
| // still be strictly less than the memstore's earliest unflushed edit. | ||
| assertNotEquals(HConstants.NO_SEQNUM, ids.getLastFlushedSequenceId()); | ||
| assertTrue(ids.getLastFlushedSequenceId() < storeSequenceId); |
There was a problem hiding this comment.
This assertion is fragile.
It is technically off by one, but because the region open marker consumes one seq id, the test will currently pass.
Maybe assertNotEquals(NO_SEQNUM, ...) instead?
There was a problem hiding this comment.
Addressed this comment in latest commit.
…t assertion Per review from @apurtell: - ServerManager.reportRegionOpen: switch putIfAbsent to merge with Math::max so a stale-low prior heartbeat value is lifted to openSeqNum rather than ignored. Safe because at OPEN a region cannot have flushed past its own openSeqNum. Guard simplified to openSeqNum < 0 (NO_SEQNUM == -1, so the disjunct was redundant). - TestGetLastFlushedSequenceId: drop the strict assertTrue(lastFlushed < storeSequenceId) - it holds only because the region-open marker consumes one seqId, so the assertion is coupled to an incidental accounting detail rather than the contract being tested. The assertNotEquals(NO_SEQNUM, ...) above captures the load-bearing invariant. Reflow adjacent javadoc block for spotless.
The seed added in reportRegionOpen (openSeqNum via Math::max) exposed a long-standing test-fixture issue: AbstractTestDLS.makeWAL uses a fresh MultiVersionConcurrencyControl that stamps WAL edits starting at seqid 1, inconsistent with the seqid sequence a real WAL preserves for the region. With the seed in place, the splitter correctly filters those low-seqid edits as already-durable and testMasterStartsUpWithLogSplittingWork loses 5/1000 rows. Advance the local MVCC past the max openSeqNum of the target regions before stamping edits, so the injected WAL entries get seqids a real region would have assigned.
Then basically there are two problems. We can do these changes in a separated issue. Thanks. |
We should check why the recovered edit files created after region open. BTW I am aware of onc case that I faced (But it looks like you faced another one) when two split worker (one zombie server and another active one) is simultaneously splitting the wal. Active one complete and we even open the region. Now zombie server can try to create one recovered-edit file from its memory.
I might not be right here but what I understand from the code is having recovered.edits files once region is open is unacceptable, and I think that was right. Even if we check that the edits are all below the persistent seqNum, we might not be sure that we didn't miss some edits. But we might not have any other way here, anyway rollback and open will do the same. |
@Apache9 Totally agree — we should have tolerance built into the merge and split procedures. I've filed a separate bug to track the issue pointed |
…eqNum seed The test previously asserted flushedSequenceIdByRegion is byte-for-byte identical across cluster shutdown+restart. After HBASE-30335 the master seeds this map on region OPEN via merge(openSeqNum, Math::max). openSeqNum is monotonic across close/open cycles, so a region reopened after restart can carry a strictly higher value than what was persisted at shutdown, and the equality assertion no longer holds. Assert the preserved invariant instead: every region persisted at shutdown is loaded on restart (same keyset) and no watermark regresses (after[r] >= before[r]). This validates persist/load correctness without conflicting with the new seed-on-open semantic.
1e14be3 to
7761ebf
Compare
There was a problem hiding this comment.
🟡 Changes recommended
A concurrent heartbeat can overwrite the newly seeded watermark with an older sequence ID.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Seeds the master’s flushed-sequence cache when a region opens, preventing unnecessary WAL recovery of durable edits.
Changes:
- Reports
openSeqNumduring region OPEN handling. - Adds unit and integration coverage.
- Updates persistence and WAL-splitting tests for the new invariant.
File summaries
| File | Description |
|---|---|
ServerManager.java |
Adds region-open sequence cache seeding. |
AssignmentManager.java |
Reports successful region opens. |
TestServerManager.java |
Tests cache seeding and monotonicity. |
TestMaster.java |
Updates restart persistence assertions. |
TestGetLastFlushedSequenceId.java |
Verifies OPEN immediately seeds the cache. |
AbstractTestDLS.java |
Aligns synthetic WAL sequence IDs with open sequence numbers. |
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Can we do this while closing of the region instread of opening the region? Looks like SCP just after the region closing is creating these recovered.edits files as RS report the flushed seq id in it heartbeats. Corrent me if I am wrong. |
CLOSE-time seeding alone doesn't cover the crash path — if the RS dies, there's no close report to seed from, so the watermark stays stale and SCP walks with the wrong fence. Doing it at OPEN time covers both cases: a graceful close-then-reopen and a crash-then-reopen-elsewhere both go through OPEN, and |
If there is no close report then master won't mvoe ahead with open so we are good in that case. We are only having a problem if edits are present after open. |
|
Fair — for a pure server crash we'd have stale data anyway, bounded by The scenario this PR is targeting is graceful move + later crash of the source RS:
Both CLOSE-time and OPEN-time seeding would fix this. Reasons I kept OPEN-time:
|
|
Copilot's review (2026-09-02, reiterated 2026-09-16) highlights a real race. This has to be fixed now, not deferred to another followup, otherwise what is the point. A stale in-flight heartbeat from the (soon to be dead) source RS can read null/a low value, race past the OPEN-seed, and then overwrite it with its lower value, reintroducing the bug this PR is meant to fix, just in a narrower window. |
…lobber the OPEN seed updateLastFlushedSequenceIds did a non-atomic get-then-put on flushedSequenceIdByRegion. A stale in-flight heartbeat from the soon-to-be-dead source RS could read null/a low value, race past the reportRegionOpen seed (merge/Math::max), and then put its lower value on top - reintroducing the stale-fence bug this PR fixes, in a narrower window (flagged by Copilot's review). Replace both the region- and store-level updates with an atomic compute that keeps the same "never lower the watermark" rule. Every writer on the live serving path is now an atomic max-merge, so the stored value is monotonic non-decreasing and a concurrent seed can no longer be clobbered. Add TestServerManager.testConcurrentStaleHeartbeatDoesNotClobberOpenSeed, which drives the OPEN seed and a stale heartbeat concurrently over 500 rounds. It fails on the pre-fix code and passes with the fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@apurtell Got it. The race was in updateLastFlushedSequenceIds: it did a non-atomic get-then-put, so a stale in-flight heartbeat could read null/low, race past the reportRegionOpen seed, and put its lower value on top. Both the region- and store-level updates now go through an atomic compute that keeps the existing "never lower the watermark" rule. With the merge(Math::max) seed on the OPEN side, every writer on the live serving path is now an atomic max-merge — the stored value is monotonic non-decreasing, so a concurrent seed can no longer be clobbered. Whatever the interleaving: heartbeat-before-seed → the seed lifts it to openSeqNum; heartbeat-after-seed → compute refuses to lower it. Added TestServerManager#testConcurrentStaleHeartbeatDoesNotClobberOpenSeed to test the scenerio. |
Summary
flushedSequenceIdByRegionis only updated via periodic RegionServer heartbeats. On a fresh region OPEN, the entry is absent, soServerManager.getLastFlushedSequenceIdreturnsNO_SEQNUMandWALSplittertreats every WAL edit as un-flushed. If the source RS of a drain-move crashes before its next heartbeat, this produces orphanedrecovered.editsfiles containing already-durable edits, which then surface as false-positive "data loss" warnings during subsequent merge/split operations and leave regions stuck in RIT.ServerManager.reportRegionOpen(regionInfo, openSeqNum)and call it fromAssignmentManager.regionOpenedWithoutPersistingToMetaso the master seeds its flushed-sequence cache synchronously with theopenSeqNumreported on the OPEN transition.putIfAbsentis used so a heartbeat-supplied value (which may reflect flushes after open) is never regressed.JIRA: https://issues.apache.org/jira/browse/HBASE-30335
Test plan
TestAssignmentManagerandTestServerManagersuites pass.recovered.editsare produced for already-flushed sequence numbers and no downstream stuck RIT on a follow-up merge.