This is an automated email from the ASF dual-hosted git repository.

dengzhhu653 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git


The following commit(s) were added to refs/heads/master by this push:
     new 18628fde9f5 HIVE-29720: Abort the service if Metastore cannot 
participate in the election (#6621)
18628fde9f5 is described below

commit 18628fde9f53592d3dad924ef215e8513a4337b0
Author: Arun <[email protected]>
AuthorDate: Fri Jul 31 05:28:41 2026 +0530

    HIVE-29720: Abort the service if Metastore cannot participate in the 
election (#6621)
---
 .../hadoop/hive/metastore/HiveMetaStore.java       |   3 +-
 .../metastore/leader/LeaderElectionContext.java    |  93 ++++++++++---
 .../hive/metastore/leader/TestLeaderElection.java  | 150 +++++++++++++++++++++
 3 files changed, 224 insertions(+), 22 deletions(-)

diff --git 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/HiveMetaStore.java
 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/HiveMetaStore.java
index b7a0004c76b..d6a93a75050 100644
--- 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/HiveMetaStore.java
+++ 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/HiveMetaStore.java
@@ -869,7 +869,8 @@ public void run() {
           }
           context.start();
         } catch (Throwable e) {
-          LOG.error("Failure when starting the leader tasks, Compaction or 
Housekeeping tasks may not happen", e);
+          LOG.error("Failed to initialize Metastore leader elections; 
Metastore will start without leader-only tasks "
+              + "(compaction, housekeeping). Runtime election failures will 
abort the Metastore instead.", e);
         } finally {
           startLock.unlock();
         }
diff --git 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/leader/LeaderElectionContext.java
 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/leader/LeaderElectionContext.java
index 14d18e832db..01bc44c9282 100644
--- 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/leader/LeaderElectionContext.java
+++ 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/leader/LeaderElectionContext.java
@@ -18,6 +18,7 @@
 
 package org.apache.hadoop.hive.metastore.leader;
 
+import com.google.common.annotations.VisibleForTesting;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.hive.common.TableName;
@@ -27,12 +28,14 @@
 import org.apache.hadoop.hive.metastore.conf.MetastoreConf;
 import 
org.apache.hadoop.hive.metastore.leader.LeaderElection.LeadershipStateListener;
 import org.apache.hadoop.hive.metastore.utils.MetaStoreUtils;
+import org.apache.hadoop.util.ExitUtil;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import static java.util.Objects.requireNonNull;
 
@@ -66,24 +69,27 @@ public String getName() {
 
   private final Configuration conf;
   private final String servHost;
-  // Whether the context should be started as a daemon
-  private final boolean startAsDaemon;
   // Audit the event of election
   private AuditLeaderListener auditLeaderListener;
   // State change listeners group by type
   private final Map<TTYPE, List<LeadershipStateListener>> listeners;
   // Collection of leader candidates
   private final List<LeaderElection<?>> leaderElections = new ArrayList<>();
-  // Property for testing, a single leader will be created
+  // Ensures the abort action runs at most once even if both electors fail 
concurrently.
+  private final AtomicBoolean isAborting = new AtomicBoolean(false);
+  // Action to run when a leader election exhausts retries. Defaults to 
ExitUtil.terminate,
+  // which fires the HMS shutdown-hook chain (election close, ZK 
deregistration, metrics,
+  // servlet server). ExitUtil is preferred over raw System.exit for JDK 21 
compatibility
+  // and test-time exit interception. Overridable for tests.
+  private Runnable abortAction = () -> ExitUtil.terminate(1);
 
   private LeaderElectionContext(String servHost, Configuration conf,
       Map<TTYPE, List<LeadershipStateListener>> listeners,
-      boolean startAsDaemon, IHMSHandler handler) throws Exception {
+      IHMSHandler handler) throws Exception {
     requireNonNull(conf, "conf is null");
     requireNonNull(listeners, "listeners is null");
     this.servHost = servHost;
     this.conf = new Configuration(conf);
-    this.startAsDaemon = startAsDaemon;
     String tableName = MetastoreConf.getVar(conf,
         MetastoreConf.ConfVars.METASTORE_HOUSEKEEPING_LEADER_AUDITTABLE);
     if (StringUtils.isNotEmpty(tableName)) {
@@ -97,6 +103,7 @@ private LeaderElectionContext(String servHost, Configuration 
conf,
   public void start() throws Exception {
     List<TTYPE> ttypes = new ArrayList<>(listeners.keySet());
     Collections.shuffle(ttypes);
+    List<Thread> daemons = new ArrayList<>();
     for (TTYPE ttype : ttypes) {
       List<LeadershipStateListener> listenerList = listeners.get(ttype);
       if (listenerList.isEmpty()) {
@@ -117,19 +124,24 @@ public void start() throws Exception {
           throw new RuntimeException("Error claiming to be leader: " + 
leaderElection.getName(), e);
         }
       });
-
-      if (startAsDaemon) {
-        daemon.setName("Metastore Election " + leaderElection.getName());
-        daemon.setDaemon(true);
-        daemon.start();
-      } else {
-        daemon.run();
-      }
+      daemon.setName("Metastore Election " + leaderElection.getName());
+      daemon.setDaemon(true);
+      
daemon.setUncaughtExceptionHandler(newAbortOnElectionFailureHandler(leaderElection));
+      daemon.start();
+      daemons.add(daemon);
+    }
+    // Wait for each initial tryBeLeader() to complete so listeners have fired 
before
+    // start() returns. Daemons are started before joining so elections still 
run in parallel.
+    for (Thread daemon : daemons) {
+      daemon.join();
     }
   }
 
   public void close() {
-    leaderElections.forEach(le -> {
+    // Iterate over a snapshot: closing an election may fire listener 
callbacks that
+    // touch the context, which could mutate leaderElections mid-iteration.
+    List<LeaderElection<?>> snapshot = new ArrayList<>(leaderElections);
+    snapshot.forEach(le -> {
       try {
         le.close();
       } catch (Exception e) {
@@ -138,9 +150,53 @@ public void close() {
     });
   }
 
+  /**
+   * Builds the {@link Thread.UncaughtExceptionHandler} attached to each 
election daemon.
+   * When the daemon terminates because {@code tryBeLeader} exhausted its 
retries, this
+   * handler logs the failure and invokes {@link #abortAction}. The {@link 
AtomicBoolean}
+   * guard ensures the abort runs at most once even if both electors fail 
concurrently.
+   */
+  private Thread.UncaughtExceptionHandler newAbortOnElectionFailureHandler(
+      LeaderElection<?> leaderElection) {
+    return (t, ex) -> {
+      HiveMetaStore.LOG.error(
+          "Metastore leader election '{}' failed after {} retries in thread 
'{}'; "
+              + "aborting Metastore to avoid running with unelected tasks.",
+          leaderElection.getName(),
+          MetastoreConf.getIntVar(conf, 
MetastoreConf.ConfVars.LOCK_NUMRETRIES),
+          t.getName(),
+          ex);
+      if (isAborting.compareAndSet(false, true)) {
+        // Best-effort close the electors so the mutex/lease is released 
promptly,
+        // letting other HMS instances proceed with election rather than 
waiting for
+        // the JVM shutdown hook to release resources.
+        try {
+          close();
+        } catch (Exception e) {
+          HiveMetaStore.LOG.warn("Error closing elections during abort", e);
+        }
+        try {
+          abortAction.run();
+        } catch (Exception e) {
+          HiveMetaStore.LOG.error("Error while executing Metastore abort 
action", e);
+        }
+      }
+    };
+  }
+
+  /**
+   * Overrides the action taken when a leader election exhausts its retries.
+   * Production code should leave the default ({@code System.exit(1)}), which
+   * triggers the HMS orderly-shutdown hook chain. Tests can inject a fake to
+   * assert the abort path fires without killing the test JVM.
+   */
+  @VisibleForTesting
+  void setAbortAction(Runnable abortAction) {
+    this.abortAction = requireNonNull(abortAction, "abortAction is null");
+  }
+
   public static class ContextBuilder {
     private Configuration configuration;
-    private boolean startAsDaemon;
     private String servHost;
     private IHMSHandler handler;
     private TTYPE ttype = TTYPE.HOUSEKEEPING;
@@ -183,14 +239,9 @@ public ContextBuilder addListener(
       return this;
     }
 
-    public ContextBuilder startAsDaemon(boolean daemon) {
-      this.startAsDaemon = daemon;
-      return this;
-    }
-
     public LeaderElectionContext build() throws Exception {
       return new LeaderElectionContext(servHost, configuration,
-          listeners, startAsDaemon, handler);
+          listeners, handler);
     }
   }
 }
diff --git 
a/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/leader/TestLeaderElection.java
 
b/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/leader/TestLeaderElection.java
index c3d24cd2dce..8d7b8dd28bc 100644
--- 
a/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/leader/TestLeaderElection.java
+++ 
b/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/leader/TestLeaderElection.java
@@ -20,16 +20,23 @@
 
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.hive.common.TableName;
+import org.apache.hadoop.hive.metastore.annotation.MetastoreUnitTest;
 import org.apache.hadoop.hive.metastore.conf.MetastoreConf;
 import org.apache.hadoop.hive.metastore.utils.TestTxnDbUtil;
 import org.junit.Test;
+import org.junit.experimental.categories.Category;
 
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+@Category(MetastoreUnitTest.class)
 public class TestLeaderElection {
 
   @Test
@@ -107,4 +114,147 @@ public void testLeaseLeaderElection() throws Exception {
     assertTrue(flag2.get() && instance2.isLeader());
   }
 
+  /**
+   * HIVE-29720: when a leader election exhausts its retries the daemon thread
+   * previously died silently, leaving HMS running without housekeeping tasks.
+   * LeaderElectionContext now installs an UncaughtExceptionHandler that 
invokes
+   * an abort action (System.exit(1) in production). This test injects a fake
+   * abort action to assert the handler fires when an election fails.
+   */
+  @Test
+  public void testAbortActionInvokedWhenElectionFails() throws Exception {
+    AtomicInteger invocations = new AtomicInteger(0);
+    LeaderElectionFactory.ElectionCreator originalHostCreator =
+        conf -> new StaticLeaderElection();
+    LeaderElectionFactory.addElectionCreator(
+        LeaderElectionFactory.Method.HOST, conf -> new 
FailingLeaderElection(invocations));
+    try {
+      Configuration conf = newAbortTestConf();
+      CountDownLatch abortLatch = new CountDownLatch(1);
+
+      LeaderElectionContext context = new 
LeaderElectionContext.ContextBuilder(conf)
+          .servHost("test-host")
+          .setTType(LeaderElectionContext.TTYPE.HOUSEKEEPING)
+          .addListener(new TestLeaderListener(new AtomicBoolean(false)))
+          .build();
+      context.setAbortAction(abortLatch::countDown);
+
+      context.start();
+
+      assertTrue("HMS should trigger abort action when leader election fails",
+          abortLatch.await(10, TimeUnit.SECONDS));
+      assertEquals("Failing election should have been invoked once",
+          1, invocations.get());
+    } finally {
+      LeaderElectionFactory.addElectionCreator(
+          LeaderElectionFactory.Method.HOST, originalHostCreator);
+    }
+  }
+
+  /**
+   * HIVE-29720: when both electors (HOUSEKEEPING and ALWAYS_TASKS) fail near
+   * simultaneously, the abort action must still run at most once. Guarded by
+   * an AtomicBoolean in LeaderElectionContext.
+   */
+  @Test
+  public void testAbortActionInvokedOnceWhenBothElectorsFail() throws 
Exception {
+    AtomicInteger invocations = new AtomicInteger(0);
+    LeaderElectionFactory.ElectionCreator originalHostCreator =
+        conf -> new StaticLeaderElection();
+    LeaderElectionFactory.addElectionCreator(
+        LeaderElectionFactory.Method.HOST, conf -> new 
FailingLeaderElection(invocations));
+    try {
+      Configuration conf = newAbortTestConf();
+      AtomicInteger abortCount = new AtomicInteger(0);
+      CountDownLatch firstAbortLatch = new CountDownLatch(1);
+
+      LeaderElectionContext context = new 
LeaderElectionContext.ContextBuilder(conf)
+          .servHost("test-host")
+          .setTType(LeaderElectionContext.TTYPE.HOUSEKEEPING)
+          .addListener(new TestLeaderListener(new AtomicBoolean(false)))
+          .setTType(LeaderElectionContext.TTYPE.ALWAYS_TASKS)
+          .addListener(new TestLeaderListener(new AtomicBoolean(false)))
+          .build();
+      context.setAbortAction(() -> {
+        abortCount.incrementAndGet();
+        firstAbortLatch.countDown();
+      });
+
+      context.start();
+
+      assertTrue("HMS should trigger abort action when either election fails",
+          firstAbortLatch.await(10, TimeUnit.SECONDS));
+
+      // Wait briefly so any race between the two failing daemons resolves,
+      // then verify the AtomicBoolean guard kept abortAction at a single 
invocation.
+      Thread.sleep(500);
+
+      assertEquals("Abort action must run exactly once even if both electors 
fail",
+          1, abortCount.get());
+      assertEquals("Both failing electors should have been invoked",
+          2, invocations.get());
+    } finally {
+      LeaderElectionFactory.addElectionCreator(
+          LeaderElectionFactory.Method.HOST, originalHostCreator);
+    }
+  }
+
+  private static Configuration newAbortTestConf() {
+    Configuration conf = MetastoreConf.newMetastoreConf();
+    // Use the HOST election method so we don't need a real database mutex.
+    MetastoreConf.setVar(conf,
+        MetastoreConf.ConfVars.METASTORE_HOUSEKEEPING_LEADER_ELECTION, "HOST");
+    // Clear the audit table so LeaderElectionContext doesn't try to build an
+    // AuditLeaderListener against a live handler in this unit test.
+    MetastoreConf.setVar(conf,
+        MetastoreConf.ConfVars.METASTORE_HOUSEKEEPING_LEADER_AUDITTABLE, "");
+    return conf;
+  }
+
+  /**
+   * A {@link LeaderElection} that unconditionally throws {@link 
LeaderException}
+   * from {@code tryBeLeader}, simulating retry exhaustion in the real
+   * {@code LeaseLeaderElection}. Registered via
+   * {@link LeaderElectionFactory#addElectionCreator} for the duration of a 
test.
+   */
+  static final class FailingLeaderElection implements LeaderElection<Object> {
+    private final AtomicInteger invocations;
+    private String name;
+
+    FailingLeaderElection(AtomicInteger invocations) {
+      this.invocations = invocations;
+    }
+
+    @Override
+    public void tryBeLeader(Configuration conf, Object mutex) throws 
LeaderException {
+      invocations.incrementAndGet();
+      throw new LeaderException("Simulated retry exhaustion for election: " + 
name);
+    }
+
+    @Override
+    public boolean isLeader() {
+      return false;
+    }
+
+    @Override
+    public void addStateListener(LeaderElection.LeadershipStateListener 
listener) {
+      // no-op
+    }
+
+    @Override
+    public void setName(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String getName() {
+      return name;
+    }
+
+    @Override
+    public void close() throws IOException {
+      // no-op
+    }
+  }
+
 }

Reply via email to