Skip to content

HIVE-29720: Abort the service if Metastore cannot participate in the election#6621

Open
arunk-kumar wants to merge 5 commits into
apache:masterfrom
arunk-kumar:HIVE-29720-abort-hms-on-election-failure
Open

HIVE-29720: Abort the service if Metastore cannot participate in the election#6621
arunk-kumar wants to merge 5 commits into
apache:masterfrom
arunk-kumar:HIVE-29720-abort-hms-on-election-failure

Conversation

@arunk-kumar

Copy link
Copy Markdown

What changes were proposed in this pull request?

Install an UncaughtExceptionHandler on each election daemon in LeaderElectionContext.start(). On retry exhaustion it logs the failing election at ERROR and calls an injectable abortAction (default System.exit(1)), which triggers the existing HMS shutdown-hook chain (election close(), ZooKeeper deregistration, metrics, servlet server). An AtomicBoolean guards against a second abort when both electors fail concurrently. Also adds @Category(MetastoreUnitTest.class) to TestLeaderElection — the class was previously excluded by the module's test.groups filter.

Why are the changes needed?

Retry exhaustion in LeaseLeaderElection previously terminated the daemon thread with no UncaughtExceptionHandler — no log, no audit, no shutdown. HMS kept running with no leader for HOUSEKEEPING / ALWAYS_TASKS, silently stalling compaction and stats updates until users noticed via query slowdowns.

Does this PR introduce any user-facing change?

Yes. When leader election exhausts metastore.lock.numretries retries, HMS now shuts down cleanly instead of remaining up without a leader for its housekeeping / always-tasks work. The existing retry knobs remain the tolerance dial.

How was this patch tested?

Added two unit tests to TestLeaderElection that inject a failing LeaderElection and a fake abortAction, asserting the handler fires and that concurrent failures still trigger the abort exactly once. Full class runs in ~4.5s: Tests run: 4, Failures: 0, Errors: 0, Skipped: 0.

…election

Retry exhaustion in LeaseLeaderElection previously terminated the
election daemon thread with an uncaught RuntimeException and no
UncaughtExceptionHandler, leaving HMS running with no leader for the
HOUSEKEEPING or ALWAYS_TASKS category. Install an UncaughtExceptionHandler
that logs the failing election and calls an injectable abort action
(System.exit(1) in production, which triggers the existing HMS shutdown
hooks). An AtomicBoolean ensures the abort runs at most once when both
electors fail concurrently.

Also adds @category(MetastoreUnitTest.class) to TestLeaderElection so its
tests are picked up by the module's test.groups filter.
Extract the election-daemon UncaughtExceptionHandler into
newAbortOnElectionFailureHandler() to drop start()'s cognitive
complexity below the 15 threshold, and narrow the abortAction
catch from Throwable to Exception. Also rewords a test comment
that SonarCloud misidentified as commented-out code.
Always start each election candidate in its own daemon thread so
the UncaughtExceptionHandler fires on retry exhaustion regardless of
how LeaderElectionContext is constructed. The startAsDaemon flag and
its builder setter are removed since no caller sets it to true in
production and the synchronous run() path bypassed the abort handler.

Also updates HiveMetaStore's error message on election setup failure
to reflect the new behavior (setup failure vs. runtime failure).
@arunk-kumar

Copy link
Copy Markdown
Author

Thanks for the review! Pushed a commit addressing both points:

  • start() now unconditionally starts each election candidate in its own daemon thread, so the UncaughtExceptionHandler fires regardless of how the context is constructed. The startAsDaemon builder flag and its backing field are removed since no production caller sets it to true and the synchronous run() path bypassed the abort handler.
  • Updated the error message in HiveMetaStore to distinguish election setup failures from runtime election failures.

TestLeaderElection still passes locally: Tests run: 4, Failures: 0, Errors: 0, Skipped: 0.

Per review feedback:
- Default abortAction now calls ExitUtil.terminate(1) instead of System.exit(1),
  matching the pattern established by HIVE-29056 for JDK 21 compatibility.
- The abort handler now best-effort closes the registered electors before
  invoking abortAction, so the mutex/lease is released promptly and other
  HMS instances can proceed with election.
- close() iterates a snapshot of leaderElections to avoid concurrent
  modification if a close listener touches the context.
@arunk-kumar

Copy link
Copy Markdown
Author

Thanks @dengzhhu653 — both addressed in the latest commit:

  • Switched the default abortAction from System.exit(1) to ExitUtil.terminate(1), following the HIVE-29056 pattern.
  • Abort handler now closes the electors before invoking abortAction, so the mutex is released promptly. close() iterates a snapshot copy of leaderElections to avoid mid-iteration mutation as you suggested.

TestLeaderElection still passes: Tests run: 4, Failures: 0, Errors: 0, Skipped: 0.

@dengzhhu653

Copy link
Copy Markdown
Member

Sounds like the CI failures are related, e.g, TestMetastoreHousekeepingLeaderEmptyConfig, can you please take a look? Thank you!

The switch from daemon.run() to daemon.start() in a previous commit
broke a pre-existing invariant that LeaderElectionContext.start()
would not return until listeners had fired synchronously. Fixing
TestMetastoreHousekeepingLeaderEmptyConfig, which scans for
housekeeping threads immediately after HMS starts.

Fix: join each daemon after starting all of them. Elections still
run in parallel (all daemons are started before the first join),
but start() waits until every initial tryBeLeader() completes.
LeaseLeaderElection.tryBeLeader returns after doWork() dispatches
to a separate Heartbeater/NonLeaderWatcher, so join() terminates
in bounded time in every case.
@arunk-kumar

Copy link
Copy Markdown
Author

Thanks for the stack trace — that pinpointed it. The switch from daemon.run() (synchronous) to daemon.start() (async) in the earlier commit broke a pre-existing invariant: LeaderElectionContext.start() used to not return until listener callbacks (housekeeping thread creation etc.) had fired synchronously, and TestMetastoreHousekeepingLeaderEmptyConfig scans for those threads immediately after HMS starts.

Fix: join each daemon after starting all of them, so elections still run in parallel but start() waits until every initial tryBeLeader() completes. Since LeaseLeaderElection.tryBeLeader dispatches lease renewal to a separate Heartbeater/NonLeaderWatcher thread and then returns (rather than looping on renewal itself), join() terminates in bounded time on success, on wait-state, and on retry exhaustion.

TestLeaderElection still passes locally: Tests run: 4, Failures: 0, Errors: 0, Skipped: 0. Couldn't reproduce TestMetastoreHousekeepingLeaderEmptyConfig locally — upstream itests/hive-unit build is transiently broken on my end (Iceberg module compile issues on master). Relying on Jenkins to verify the fix.

@sonarqubecloud

Copy link
Copy Markdown

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants