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

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


The following commit(s) were added to refs/heads/master by this push:
     new 7b39b78b8ef fix: Release leadership on shutdown with k8s leader 
election. (#20800)
7b39b78b8ef is described below

commit 7b39b78b8efd779b62b4f5bd2cde5a22f3a40903
Author: Gian Merlino <[email protected]>
AuthorDate: Fri Oct 30 10:11:43 2026 -0700

    fix: Release leadership on shutdown with k8s leader election. (#20800)
    
    We were missing a call to leaderElector.close() on service stop, which
    would lead to a longer-than-necessary delay waiting for the new leader
    to take over.
---
 .../discovery/DefaultK8sLeaderElectorFactory.java  |  6 ++
 .../k8s/discovery/K8sLeaderElectorFactory.java     |  5 ++
 .../k8s/discovery/LeaderElectorAsyncWrapper.java   | 10 ++-
 .../k8s/discovery/K8sDruidLeaderSelectorTest.java  | 82 ++++++++++++++++++++++
 .../druid/indexing/overlord/DruidOverlord.java     | 32 +++++++--
 .../druid/indexing/overlord/http/OverlordTest.java | 37 +++++++++-
 .../druid/server/coordinator/DruidCoordinator.java | 10 ++-
 .../server/coordinator/DruidCoordinatorTest.java   | 16 ++++-
 8 files changed, 187 insertions(+), 11 deletions(-)

diff --git 
a/extensions-core/kubernetes-extensions/src/main/java/org/apache/druid/k8s/discovery/DefaultK8sLeaderElectorFactory.java
 
b/extensions-core/kubernetes-extensions/src/main/java/org/apache/druid/k8s/discovery/DefaultK8sLeaderElectorFactory.java
index 7ac6b5e54ba..47b4efebd42 100644
--- 
a/extensions-core/kubernetes-extensions/src/main/java/org/apache/druid/k8s/discovery/DefaultK8sLeaderElectorFactory.java
+++ 
b/extensions-core/kubernetes-extensions/src/main/java/org/apache/druid/k8s/discovery/DefaultK8sLeaderElectorFactory.java
@@ -76,6 +76,12 @@ public class DefaultK8sLeaderElectorFactory implements 
K8sLeaderElectorFactory
       {
         leaderElector.run(startLeadingHook, stopLeadingHook);
       }
+
+      @Override
+      public void close()
+      {
+        leaderElector.close();
+      }
     };
   }
 
diff --git 
a/extensions-core/kubernetes-extensions/src/main/java/org/apache/druid/k8s/discovery/K8sLeaderElectorFactory.java
 
b/extensions-core/kubernetes-extensions/src/main/java/org/apache/druid/k8s/discovery/K8sLeaderElectorFactory.java
index 10bab39f65c..e7fd307145a 100644
--- 
a/extensions-core/kubernetes-extensions/src/main/java/org/apache/druid/k8s/discovery/K8sLeaderElectorFactory.java
+++ 
b/extensions-core/kubernetes-extensions/src/main/java/org/apache/druid/k8s/discovery/K8sLeaderElectorFactory.java
@@ -38,4 +38,9 @@ interface K8sLeaderElector
 {
   String getCurrentLeader();
   void run(Runnable startLeadingHook, Runnable stopLeadingHook);
+
+  /**
+   * Stops this elector permanently and, if it holds the lock, releases it.
+   */
+  void close();
 }
diff --git 
a/extensions-core/kubernetes-extensions/src/main/java/org/apache/druid/k8s/discovery/LeaderElectorAsyncWrapper.java
 
b/extensions-core/kubernetes-extensions/src/main/java/org/apache/druid/k8s/discovery/LeaderElectorAsyncWrapper.java
index f7e1b481d04..7a791c93124 100644
--- 
a/extensions-core/kubernetes-extensions/src/main/java/org/apache/druid/k8s/discovery/LeaderElectorAsyncWrapper.java
+++ 
b/extensions-core/kubernetes-extensions/src/main/java/org/apache/druid/k8s/discovery/LeaderElectorAsyncWrapper.java
@@ -103,8 +103,14 @@ public class LeaderElectorAsyncWrapper implements Closeable
     try {
       futureRef.get().cancel(true);
       executor.shutdownNow();
-      if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
-        LOGGER.warn("Failed to terminate [%s] executor.", 
this.getClass().getSimpleName());
+      if (executor.awaitTermination(10, TimeUnit.SECONDS)) {
+        // Release the lock after the run loop has exited.
+        k8sLeaderElector.close();
+      } else {
+        LOGGER.warn(
+            "Failed to terminate [%s] executor. Not releasing the lock; it 
will expire after the lease duration.",
+            this.getClass().getSimpleName()
+        );
       }
     }
     catch (InterruptedException ex) {
diff --git 
a/extensions-core/kubernetes-extensions/src/test/java/org/apache/druid/k8s/discovery/K8sDruidLeaderSelectorTest.java
 
b/extensions-core/kubernetes-extensions/src/test/java/org/apache/druid/k8s/discovery/K8sDruidLeaderSelectorTest.java
index 8482117129e..d78f76ebd9e 100644
--- 
a/extensions-core/kubernetes-extensions/src/test/java/org/apache/druid/k8s/discovery/K8sDruidLeaderSelectorTest.java
+++ 
b/extensions-core/kubernetes-extensions/src/test/java/org/apache/druid/k8s/discovery/K8sDruidLeaderSelectorTest.java
@@ -28,6 +28,8 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
 
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -77,6 +79,11 @@ public class K8sDruidLeaderSelectorTest
                   stopLeadingHook.run();
                 }
               }
+
+              @Override
+              public void close()
+              {
+              }
             };
           }
         }
@@ -150,6 +157,11 @@ public class K8sDruidLeaderSelectorTest
                   }
                 }
               }
+
+              @Override
+              public void close()
+              {
+              }
             };
           }
         }
@@ -181,4 +193,74 @@ public class K8sDruidLeaderSelectorTest
     leaderSelector.unregisterListener();
     stopBeingLeaderLatch.await();
   }
+
+  @Test
+  @Timeout(value = 5_000, unit = TimeUnit.MILLISECONDS)
+  public void 
testLeaderElection_unregisterReleasesLockAfterStoppingLeadership() throws 
Exception
+  {
+    final List<String> events = new CopyOnWriteArrayList<>();
+
+    K8sDruidLeaderSelector leaderSelector = new K8sDruidLeaderSelector(
+        testNode1.getDruidNode(),
+        lockResourceName,
+        discoveryConfig.getCoordinatorLeaderElectionConfigMapNamespace(),
+        discoveryConfig,
+        new K8sLeaderElectorFactory()
+        {
+          @Override
+          public K8sLeaderElector create(String candidateId, String namespace, 
String lockResourceName)
+          {
+            return new K8sLeaderElector()
+            {
+              @Override
+              public String getCurrentLeader()
+              {
+                return testNode1.getDruidNode().getHostAndPortToUse();
+              }
+
+              @Override
+              public void run(Runnable startLeadingHook, Runnable 
stopLeadingHook)
+              {
+                startLeadingHook.run();
+                try {
+                  Thread.sleep(Long.MAX_VALUE);
+                }
+                catch (InterruptedException ex) {
+                  stopLeadingHook.run();
+                }
+              }
+
+              @Override
+              public void close()
+              {
+                events.add("close");
+              }
+            };
+          }
+        }
+    );
+
+    CountDownLatch becomeLeaderLatch = new CountDownLatch(1);
+
+    leaderSelector.registerListener(
+        new DruidLeaderSelector.Listener()
+        {
+          @Override
+          public void becomeLeader()
+          {
+            becomeLeaderLatch.countDown();
+          }
+
+          @Override
+          public void stopBeingLeader()
+          {
+            events.add("stopBeingLeader");
+          }
+        }
+    );
+
+    becomeLeaderLatch.await();
+    leaderSelector.unregisterListener();
+    Assertions.assertEquals(List.of("stopBeingLeader", "close"), events);
+  }
 }
diff --git 
a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/DruidOverlord.java
 
b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/DruidOverlord.java
index 6e7d25168a7..bcded57e0ff 100644
--- 
a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/DruidOverlord.java
+++ 
b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/DruidOverlord.java
@@ -21,6 +21,7 @@ package org.apache.druid.indexing.overlord;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.base.Optional;
+import com.google.errorprone.annotations.concurrent.GuardedBy;
 import com.google.inject.Inject;
 import org.apache.druid.client.indexing.IndexingService;
 import org.apache.druid.discovery.DruidLeaderSelector;
@@ -69,6 +70,12 @@ public class DruidOverlord
    */
   private volatile boolean initialized;
 
+  /**
+   * Whether {@link #stop()} has been called.
+   */
+  @GuardedBy("giant")
+  private boolean stopped;
+
   @Inject
   public DruidOverlord(
       final TaskMaster taskMaster,
@@ -102,10 +109,15 @@ public class DruidOverlord
       {
         giant.lock();
 
-        // I AM THE MASTER OF THE UNIVERSE.
-        log.info("By the power of Grayskull, I have the power. I am the 
leader");
-
         try {
+          if (stopped) {
+            log.info("Not becoming leader, since this Overlord is stopped.");
+            return;
+          }
+
+          // I AM THE MASTER OF THE UNIVERSE.
+          log.info("By the power of Grayskull, I have the power. I am the 
leader");
+
           final TaskRunner taskRunner = runnerFactory.build();
           final TaskQueue taskQueue = new TaskQueue(
               taskLockConfig,
@@ -234,8 +246,20 @@ public class DruidOverlord
     giant.lock();
 
     try {
+      stopped = true;
       gracefulStopLeaderLifecycle();
-      overlordLeaderSelector.unregisterListener();
+    }
+    finally {
+      giant.unlock();
+    }
+
+    // Unregister without holding the giant lock. When using 
K8sDruidLeaderSelector, it will call
+    // stopBeingLeader() from another thread and wait for it, which needs the 
same lock.
+    overlordLeaderSelector.unregisterListener();
+
+    giant.lock();
+
+    try {
       if (isStandalone()) {
         segmentMetadataCache.stop();
       }
diff --git 
a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordTest.java
 
b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordTest.java
index 822362cf59e..1d9805b4b13 100644
--- 
a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordTest.java
+++ 
b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordTest.java
@@ -113,6 +113,7 @@ public class OverlordTest
   private Timing timing;
   private CuratorFramework curator;
   private DruidOverlord overlord;
+  private TestDruidLeaderSelector leaderSelector;
   private TaskMaster taskMaster;
   private GlobalTaskLockbox taskLockbox;
   private TaskStorage taskStorage;
@@ -243,6 +244,7 @@ public class OverlordTest
         taskActionClientFactory,
         supervisorManager
     );
+    leaderSelector = new TestDruidLeaderSelector();
     overlord = new DruidOverlord(
         taskMaster,
         new TaskLockConfig(),
@@ -256,7 +258,7 @@ public class OverlordTest
         serviceEmitter,
         supervisorManager,
         EasyMock.createNiceMock(OverlordDutyExecutor.class),
-        new TestDruidLeaderSelector(),
+        leaderSelector,
         EasyMock.createNiceMock(SegmentAllocationQueue.class),
         EasyMock.createNiceMock(SegmentMetadataCache.class),
         Set.of(
@@ -373,6 +375,23 @@ public class OverlordTest
     EasyMock.verify(taskActionClientFactory);
   }
 
+  @Timeout(60)
+  @Test
+  public void testBecomeLeaderAfterStopIsIgnored() throws Exception
+  {
+    overlord.start();
+    while (!overlord.isLeader()) {
+      Thread.sleep(10);
+    }
+
+    overlord.stop();
+    Assertions.assertFalse(taskMaster.isHalfOrFullLeader());
+
+    // Simulate a becomeLeader() callback that the leader selector delivers 
after stop().
+    leaderSelector.listener.becomeLeader();
+    Assertions.assertFalse(taskMaster.isHalfOrFullLeader());
+  }
+
   /* Wait until the task with given taskId has the given Task Status
    * These method will not timeout until the condition is met so calling 
method should ensure timeout
    * This method also assumes that the task with given taskId is present
@@ -602,11 +621,25 @@ public class OverlordTest
       listener.becomeLeader();
     }
 
+    /**
+     * Calls {@link Listener#stopBeingLeader()} from another thread and waits 
for it, as K8sDruidLeaderSelector
+     * does, so callers that hold a lock needed by stopBeingLeader() fail here 
instead of deadlocking.
+     */
     @Override
     public void unregisterListener()
     {
       leader = null;
-      listener.stopBeingLeader();
+
+      final Thread thread = new Thread(listener::stopBeingLeader);
+      thread.start();
+      try {
+        thread.join(10_000);
+      }
+      catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+        throw new RuntimeException(e);
+      }
+      Assertions.assertFalse(thread.isAlive(), "stopBeingLeader() did not 
finish");
     }
   }
 }
diff --git 
a/server/src/main/java/org/apache/druid/server/coordinator/DruidCoordinator.java
 
b/server/src/main/java/org/apache/druid/server/coordinator/DruidCoordinator.java
index d385cad797a..ef4ab2244aa 100644
--- 
a/server/src/main/java/org/apache/druid/server/coordinator/DruidCoordinator.java
+++ 
b/server/src/main/java/org/apache/druid/server/coordinator/DruidCoordinator.java
@@ -401,9 +401,15 @@ public class DruidCoordinator
         return;
       }
 
-      coordLeaderSelector.unregisterListener();
-      metadataManager.stopCache();
       started = false;
+    }
+
+    // Unregister without holding the lock. When using K8sDruidLeaderSelector, 
it will call
+    // stopBeingLeader() from another thread and wait for it, which needs the 
same lock.
+    coordLeaderSelector.unregisterListener();
+
+    synchronized (lock) {
+      metadataManager.stopCache();
       stopAllDutyGroups();
     }
   }
diff --git 
a/server/src/test/java/org/apache/druid/server/coordinator/DruidCoordinatorTest.java
 
b/server/src/test/java/org/apache/druid/server/coordinator/DruidCoordinatorTest.java
index c3203df0a93..a310957a68d 100644
--- 
a/server/src/test/java/org/apache/druid/server/coordinator/DruidCoordinatorTest.java
+++ 
b/server/src/test/java/org/apache/druid/server/coordinator/DruidCoordinatorTest.java
@@ -1127,11 +1127,25 @@ public class DruidCoordinatorTest
       listener.becomeLeader();
     }
 
+    /**
+     * Calls {@link Listener#stopBeingLeader()} from another thread and waits 
for it, as K8sDruidLeaderSelector
+     * does, so callers that hold a lock needed by stopBeingLeader() fail here 
instead of deadlocking.
+     */
     @Override
     public void unregisterListener()
     {
       leader = null;
-      listener.stopBeingLeader();
+
+      final Thread thread = new Thread(listener::stopBeingLeader);
+      thread.start();
+      try {
+        thread.join(10_000);
+      }
+      catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+        throw new RuntimeException(e);
+      }
+      Assertions.assertFalse(thread.isAlive(), "stopBeingLeader() did not 
finish");
     }
   }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to