Skip to content

HBASE-29555 TestRollbackSCP.testFailAndRollback fails sometimes because non empty Scheduler queue#8357

Open
SwaraliJoshi wants to merge 3 commits into
apache:masterfrom
SwaraliJoshi:HBASE-29555
Open

HBASE-29555 TestRollbackSCP.testFailAndRollback fails sometimes because non empty Scheduler queue#8357
SwaraliJoshi wants to merge 3 commits into
apache:masterfrom
SwaraliJoshi:HBASE-29555

Conversation

@SwaraliJoshi

@SwaraliJoshi SwaraliJoshi commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

TestRollbackSCP.testFailAndRollback is flaky: it intermittently fails with
java.lang.IllegalArgumentException: scheduler queue not empty from
ProcedureExecutor.load() while restarting the master procedure executor.

The test restarts the ProcedureExecutor in place, reusing the same
executor and MasterProcedureScheduler instances to simulate a failover. While
the executor is being reloaded, other still-running threads of the live
mini-cluster master can push a procedure back into the shared scheduler in the
small window between scheduler.clear() and ProcedureExecutor.load()'s
Preconditions.checkArgument(scheduler.size() == 0, ...).

Two producers were identified:

  • the asyncTaskExecutor callback that wakes a procedure after an async meta
    update (e.g. AssignmentManager.persistToMeta), and
  • an incoming reportRegionStateTransition RPC from a live region server,
    handled on an RpcServer handler thread, which wakes a procedure through
    ProcedureEvent.wake -> scheduler.addFront.

This is a test-infrastructure issue: a real master failover starts a fresh
process with a fresh executor/scheduler, so the production load() precondition
is not affected. The fix is therefore confined to test code.

Solution (ProcedureTestingUtility.restart(), test-only)

The fix makes the test's in-place reload faithfully reproduce a real master failover by closing the two gaps, with no retry loop.

  1. Gap 1 — no stale async wake survives the reload (awaitAsyncTaskExecutorTermination)
    A new helper that, after the executor is stopped (which shuts the asyncTaskExecutor down), waits for that executor to fully terminate (bounded 60s) before the reload clears the scheduler. This guarantees any pending persistToMeta/regionClosedAbnormally wake-up callback has finished, and any future completing afterward is rejected by the shut-down executor. It reproduces production's guarantee that "the old process's async work is gone before load()."

  2. Gap 2 — no region report during the reload (RELOAD_LOCK + report override)
    A fair ReentrantReadWriteLock RELOAD_LOCK.
    AssignmentManagerForTest.reportRegionStateTransition(...) is overridden to take the read lock via non-blocking tryLock; if the reload holds the write lock it throws PleaseHoldException (the RegionServer retries). This mirrors production exactly — MasterRpcServices.checkServiceStarted() rejects reports until the master finishes initializing (which is after load()). tryLock (not a blocking lock) is deliberate: it makes a report never block, so the AM re-init inside the reload can't deadlock behind a blocked report, and RPC handler threads stay free.

Test plan

  • Ran TestRollbackSCP 100x consecutively with the fix: 100/100 passed (twice).
  • Control: reverted the fix and reran: reproduced the failure within a few iterations.
  • Regression: ran affected hbase-procedure tests and a representative
    hbase-server subset (TestSCP, TestProcedureAdmin,
    TestTransitRegionStateProcedure, TestCreateTableProcedure): all pass.

…se non empty Scheduler queue

TestRollbackSCP restarts the master ProcedureExecutor in place, reusing the
same executor and MasterProcedureScheduler instances to simulate a failover.
While the executor is being reloaded, other still-running threads of the live
mini-cluster master can push a procedure back into the shared scheduler in the
window between clearing it and ProcedureExecutor.load() asserting that it is
empty, so load() intermittently fails with "scheduler queue not empty".

There are two such producers:
- the asyncTaskExecutor callback that wakes a procedure after an async meta
  update (e.g. persistToMeta), and
- an incoming reportRegionStateTransition RPC from a live region server, handled
  on an RpcServer thread, which wakes a procedure via ProcedureEvent.

Fix ProcedureTestingUtility.restart() (test-only):
- wait for the already shutdown asyncTaskExecutor to fully terminate before
  clearing the scheduler, so any pending wake-up callback has finished; and
- reload (clear -> store start -> init) in a bounded retry loop, retrying only
  when load() fails because the scheduler is not empty. ProcedureExecutor.stop()
  is explicitly safe to call after a failed init(), so this is a clean redo.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Apache9

Apache9 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

I prefer we just change the code the TestRollbackSCP.

We have lots of other tests which rely on this restart, changing the logic may hide some other bugs we want to expose in these tests.

Thanks.

@SwaraliJoshi

Copy link
Copy Markdown
Contributor Author

I prefer we just change the code the TestRollbackSCP.

We have lots of other tests which rely on this restart, changing the logic may hide some other bugs we want to expose in these tests.

Thanks.

@Apache9 Yes, I am checking if we have missed any condition to stimulate the real production restart of master due to which this issue is happening only in the test. Please allow me sometime to make the necessary changes.

// scheduler is not empty, stop/drain/clear again and retry. ProcedureExecutor.stop() is
// explicitly safe to call after a failed init(), so this is a clean redo of the reload.
final int maxRestartAttempts = 20;
for (int attempt = 1;; attempt++) {

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.

Emptying scheduler is not out goal here. Check cause of this and try to resolve that. If we are missing something while simulating the Master restart lets add that.

SwaraliJoshi and others added 2 commits June 25, 2026 02:47
…ailover

Fix the flaky TestRollbackSCP.testFailAndRollback ("IllegalArgumentException:
scheduler queue not empty" from ProcedureExecutor.load()) entirely within the
test, by making the in-place procedure-executor reload reproduce the two
guarantees a real master failover provides instead of relying on a retry:

- Gap #1: after stopping the executor, wait for its asyncTaskExecutor to fully
  terminate before reloading, so a pending async meta-update wake-up cannot
  re-populate the scheduler during load(). A real failover starts a fresh
  process, so no such callback survives.
- Gap apache#2: hold a fair ReentrantReadWriteLock write lock across the reload, and
  have reportRegionStateTransition take the read lock via non-blocking tryLock,
  rejecting reports with PleaseHoldException during the reload (the region
  server retries). This mirrors MasterRpcServices.checkServiceStarted(), which
  rejects region reports until the master finishes initializing, i.e. after
  load(). Acquiring the write lock also waits for any in-flight report.

The shared ProcedureTestingUtility.restart() is left unchanged so the other
tests that rely on it keep their strict scheduler-empty invariant.

Co-authored-by: Cursor <cursoragent@cursor.com>
// reporting region state transitions. See restartMasterProcedureExecutorLikeFailover(). Fair so
// that, while the reload is waiting for / holding the write lock, new reports are rejected rather
// than barging in. A region state report takes the read lock; the reload takes the write lock.
private static final ReentrantReadWriteLock RELOAD_LOCK = new ReentrantReadWriteLock(true);

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.

Actually I do not get the point why here we need fair lock?

// reload; acquiring the write lock in the reload still waits for any already in-flight report.
boolean locked = false;
try {
locked = RELOAD_LOCK.readLock().tryLock(0, TimeUnit.SECONDS);

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.

What does timeout 0 means here? No timeout or same with tryLock without any parameters?

}
long deadline = System.currentTimeMillis() + 60000;
try {
while (!asyncTaskExecutor.awaitTermination(1, TimeUnit.SECONDS)) {

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.

Why not just wait for 1 minute?

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