Skip to content

HBASE-30357 OpenRegionProcedure#restoreSucceedState ignores persisted transitionCode, forcing OPEN even after a real FAILED_OPEN - #8622

Open
mnpoonia wants to merge 1 commit into
apache:masterfrom
mnpoonia:HBASE-30357-restoreSucceedState-failed-open
Open

mnpoonia wants to merge 1 commit into
apache:masterfrom
mnpoonia:HBASE-30357-restoreSucceedState-failed-open

Conversation

@mnpoonia

@mnpoonia mnpoonia commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

OpenRegionProcedure#restoreSucceedState() is called on master-failover restore, once
per region, via RegionRemoteProcedureBase#stateLoaded(). It unconditionally forced the
region into OPEN, regardless of the actual persisted transitionCode, which can be
FAILED_OPEN. The method's signature only received seqId, not transitionCode, so it
was structurally unable to branch on the real outcome.

Concretely: an RS reports FAILED_OPEN; the master persists
state=REPORT_SUCCEED, transitionCode=FAILED_OPEN on the OpenRegionProcedure, but the
in-memory RegionState.State is left untouched (still OPENING, since
AssignmentManager#regionFailedOpen(regionNode, false) on the live path does not update
RegionState.State). If the master crashes/restarts before this reaches confirmOpened()
and before hbase:meta is updated, restoreSucceedState() sees the region isn't OPEN
yet and force-transitions it to OPEN anyway, then persists that (false) OPEN state to
hbase:meta. There is no rollback anywhere in this procedure chain (by design -
forward-only), so nothing downstream can detect or correct it; the region silently looks
healthy in meta while no RegionServer is actually serving it.

By contrast, CloseRegionProcedure#restoreSucceedState() is safe doing the equivalent
unconditional force, because CLOSE has no failure-variant transition code at the master
side (checkTransition/updateTransitionWithoutPersistingToMeta both assert
transitionCode == CLOSED).

This is not a regression - the logic is unchanged (modulo spotless formatting) since it
was introduced in HBASE-22365 (2019-05-10).

The fix

  • Widen RegionRemoteProcedureBase#restoreSucceedState() to also receive the persisted
    transitionCode, passed through from stateLoaded().
  • OpenRegionProcedure#restoreSucceedState() now branches: FAILED_OPEN calls
    AssignmentManager#regionFailedOpen(regionNode, false), mirroring exactly what the live
    reportTransition/updateTransitionWithoutPersistingToMeta path already does for the
    same transition code; the existing OPENED handling is unchanged.
  • CloseRegionProcedure#restoreSucceedState() accepts the new parameter and ignores it,
    since CLOSE has no failure variant.

After the fix, a restore-time FAILED_OPEN puts the region back into the same
retryable path (OPENING, cleared location) that TransitRegionStateProcedure#confirmOpened()
already drives on the live path - the region gets reassigned/retried through the normal
flow instead of being falsely marked OPEN.

Why are the changes needed?

To prevent a region from being silently, durably marked OPEN in hbase:meta after a
master restart, when in reality no RegionServer opened it. Since HBase has no rollback
mechanism for these forward-only assignment procedures, this bug is otherwise
unrecoverable except by manual detection and intervention.

Does this PR introduce any user-facing change?

No.

Is there a corresponding Apache JIRA?

Yes: HBASE-30357

How was this patch tested?

Added TestOpenRegionProcedureRestoreFailedOpen, extending TestAssignmentManagerBase. A
custom mock RS executor reports FAILED_OPEN on the first open attempt, then - while
holding the RegionStateNode lock, simulating the point right after a crash where the
child procedure has not yet resumed its own execute() - directly invokes the
TransitRegionStateProcedure#stateLoaded() hook that a real master restart would trigger,
and records the region's state immediately after. Confirmed the test fails against
unmodified code with the exact predicted OPEN state, and passes after the fix. Also ran
TestAssignmentManager, TestTransitRegionStateProcedure, TestOpenRegionProcedureHang,
TestOpenRegionProcedureBackoff, TestRollbackSCP, and TestSCPGetRegionsRace with no
regressions.

@mnpoonia
mnpoonia force-pushed the HBASE-30357-restoreSucceedState-failed-open branch from 2203eb4 to e2cde5d Compare September 5, 2026 05:58
@mnpoonia

mnpoonia commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

@virajjasani @apurtell @Apache9 Can you please help with review.

… transitionCode, forcing OPEN even after a real FAILED_OPEN

On master-failover restore, RegionRemoteProcedureBase.stateLoaded() calls
restoreSucceedState() for any region whose remote-open report was already
persisted with state REGION_REMOTE_PROCEDURE_REPORT_SUCCEED, but before this
change OpenRegionProcedure ignored the real persisted TransitionCode and
always forced the region to OPEN. If the RS had actually reported
FAILED_OPEN and the master crashed before persisting that to hbase:meta, the
region would come back as a "phantom" OPEN region on restart: no procedure
watching it, no automatic retry, and invisible to the RegionInTransition
tracker since OPEN is the only non-RIT terminal state.

Widen RegionRemoteProcedureBase#restoreSucceedState to also receive the
persisted TransitionCode, and make OpenRegionProcedure branch on it: on
FAILED_OPEN, call AssignmentManager#regionFailedOpen(regionNode, false),
mirroring what the live (non-restart) reportTransition path already does.
This leaves the region non-OPEN so TransitRegionStateProcedure#confirmOpened
sees it and drives the normal retry loop instead of finishing silently.
CloseRegionProcedure's override is updated to match the new signature but
ignores the parameter, since CLOSE has no failure variant.

Add TestOpenRegionProcedureRestoreFailedOpen, which reproduces the restore
timing directly (invoking the same package-private stateLoaded() hook that
a real master restart triggers) and asserts the region is not left in OPEN
state after a FAILED_OPEN report.
@mnpoonia
mnpoonia force-pushed the HBASE-30357-restoreSucceedState-failed-open branch from ded91c9 to 89a3dfb Compare September 5, 2026 06:32
@mnpoonia

mnpoonia commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

@Apache9 can you please help in reviewing.

@virajjasani virajjasani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one, this is worth fixing

Comment on lines +131 to +132
if (transitionCode == TransitionCode.FAILED_OPEN) {
// will not persist to meta if giveUp is false, matches the live reportTransition path

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is FAILED_OPEN the only case where we have this problem? What about split/merge reverted states?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question — I checked both cases:

CloseRegionProcedure: no analogous bug possible. CloseRegionProcedure.checkTransition only ever accepts CLOSED (CloseRegionProcedure.java:99-106) — there's no FAILED_CLOSE/revert code in the protocol at all (the RS aborts instead of reporting a close failure). So transitionCode is structurally guaranteed to be CLOSED whenever it's persisted for this procedure, and restoreSucceedState can't mis-restore something that only ever has one valid value.

Split/merge: these don't go through RegionRemoteProcedureBase/OpenRegionProcedure/CloseRegionProcedure/restoreSucceedState at all — they're driven by SplitTableRegionProcedure/MergeTableRegionsProcedure and reported via a separate switch in AssignmentManager.reportRegionStateTransition, which routes READY_TO_SPLIT/READY_TO_MERGE to their own handlers. Those handlers reject SPLIT/MERGED/SPLIT_REVERTED/MERGE_REVERTED outright — those codes predate AMv2 (from the 1.x ZK-less-assignment era, HBASE-11059) and exist only as a rolling-upgrade compatibility guard against an old (<2.0) RS still reporting the legacy codes. No current RS code ever sends them.

So FAILED_OPEN is the only live instance of this restore-path gap — nothing further to fix here.

public class TestOpenRegionProcedureRestoreFailedOpen extends TestAssignmentManagerBase {

/**
* On the first open attempt, reports FAILED_OPEN and then immediately simulates a master restart

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So how do we simulate the master restart here? I haven't seen any restart in the test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. We don’t actually restart the master in this test.

The test simulates the relevant part of a master restart by directly invoking TransitRegionStateProcedure#stateLoaded(), which is the hook reached when the procedure state is loaded during master recovery.

The important sequence we want to reproduce is:

  1. RS reports FAILED_OPEN.
  2. The procedure state containing REPORT_SUCCEED + FAILED_OPEN is persisted.
  3. Master crashes before the child procedure gets a chance to continue and update the region state/meta.
  4. On recovery, stateLoaded() restores the procedure and calls restoreSucceedState().

The test holds the RegionStateNode lock after reporting FAILED_OPEN and invokes stateLoaded() directly, so the child procedure cannot continue normally. This puts us at the same point in the procedure lifecycle as the restore path after a master restart.

I can make the test/comment more explicit about this, e.g. rename the comment from “simulates a master restart” to “simulates procedure recovery after master restart”, to avoid suggesting that the test actually restarts the master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants