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

ruanwenjun pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git


The following commit(s) were added to refs/heads/dev by this push:
     new 710236bddd [Fix-18197][Master] Fix master failover lock leak (#18207)
710236bddd is described below

commit 710236bddd1bb890b6a88142bd2458504b16a45a
Author: Wenjun Ruan <[email protected]>
AuthorDate: Fri May 1 23:26:38 2026 +0800

    [Fix-18197][Master] Fix master failover lock leak (#18207)
    
    The per-master failover lock was acquired at
    /lock/master-failover/<masterAddress> but released at the parent path
    /lock/master-failover, so the per-master lock node was never released
    and blocked subsequent failover from peer masters.
    
    * RegistryClient#getLock now returns an AutoCloseable RegistryLock,
      enforcing acquire/release symmetry via try-with-resources.
    * RegistryClient#releaseLock(String) is removed; callers use the
      returned handle.
    * FailoverCoordinator#doMasterFailover stores the lock path in a local
      variable and uses try-with-resources.
---
 .../master/failover/FailoverCoordinator.java       |   7 +-
 .../master/failover/FailoverCoordinatorTest.java   | 110 +++++++++++++++++++++
 .../registry/api/RegistryClient.java               |  11 +--
 .../registry/api/RegistryLock.java                 |  53 ++++++++++
 4 files changed, 171 insertions(+), 10 deletions(-)

diff --git 
a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/failover/FailoverCoordinator.java
 
b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/failover/FailoverCoordinator.java
index 445729fe83..500ef75856 100644
--- 
a/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/failover/FailoverCoordinator.java
+++ 
b/dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/failover/FailoverCoordinator.java
@@ -22,6 +22,7 @@ import 
org.apache.dolphinscheduler.dao.entity.WorkflowInstance;
 import org.apache.dolphinscheduler.dao.repository.WorkflowInstanceDao;
 import org.apache.dolphinscheduler.plugin.task.api.enums.TaskExecutionStatus;
 import org.apache.dolphinscheduler.registry.api.RegistryClient;
+import org.apache.dolphinscheduler.registry.api.RegistryLock;
 import org.apache.dolphinscheduler.registry.api.enums.RegistryNodeType;
 import org.apache.dolphinscheduler.registry.api.utils.RegistryUtils;
 import org.apache.dolphinscheduler.server.master.cluster.ClusterManager;
@@ -139,9 +140,9 @@ public class FailoverCoordinator implements 
IFailoverCoordinator {
         // Once the FAILOVER workflow has been refired, then it's host will be 
changed to the new master and have a new
         // start time.
         // So if a master has been failovered multiple times, there is no 
problem.
+        final String masterFailoverLockPath = 
RegistryUtils.getMasterFailoverLockPath(masterAddress);
         final StopWatch failoverTimeCost = StopWatch.createStarted();
-        
registryClient.getLock(RegistryUtils.getMasterFailoverLockPath(masterAddress));
-        try {
+        try (RegistryLock ignored = 
registryClient.getLock(masterFailoverLockPath)) {
             // If the master has already been failovered, then we skip the 
failover.
             if (registryClient.exists(masterFailoverNodePath)
                     && 
String.valueOf(workflowFailoverDeadline).equals(registryClient.get(masterFailoverNodePath)))
 {
@@ -160,8 +161,6 @@ public class FailoverCoordinator implements 
IFailoverCoordinator {
                     masterAddress,
                     needFailoverWorkflows.size(),
                     failoverTimeCost.getTime());
-        } finally {
-            
registryClient.releaseLock(RegistryNodeType.MASTER_FAILOVER_LOCK.getRegistryPath());
         }
     }
 
diff --git 
a/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/failover/FailoverCoordinatorTest.java
 
b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/failover/FailoverCoordinatorTest.java
new file mode 100644
index 0000000000..558da1608f
--- /dev/null
+++ 
b/dolphinscheduler-master/src/test/java/org/apache/dolphinscheduler/server/master/failover/FailoverCoordinatorTest.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.dolphinscheduler.server.master.failover;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.dolphinscheduler.dao.repository.WorkflowInstanceDao;
+import org.apache.dolphinscheduler.registry.api.RegistryClient;
+import org.apache.dolphinscheduler.registry.api.RegistryLock;
+import org.apache.dolphinscheduler.registry.api.utils.RegistryUtils;
+import org.apache.dolphinscheduler.server.master.cluster.ClusterManager;
+import org.apache.dolphinscheduler.server.master.cluster.MasterClusters;
+import org.apache.dolphinscheduler.server.master.cluster.MasterServerMetadata;
+import org.apache.dolphinscheduler.server.master.engine.IWorkflowRepository;
+import 
org.apache.dolphinscheduler.server.master.engine.system.event.MasterFailoverEvent;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+class FailoverCoordinatorTest {
+
+    @InjectMocks
+    private FailoverCoordinator failoverCoordinator;
+
+    @Mock
+    private RegistryClient registryClient;
+
+    @Mock
+    private ClusterManager clusterManager;
+
+    @Mock
+    private IWorkflowRepository workflowRepository;
+
+    @Mock
+    private TaskFailover taskFailover;
+
+    @Mock
+    private WorkflowInstanceDao workflowInstanceDao;
+
+    @Mock
+    private WorkflowFailover workflowFailover;
+
+    @Mock
+    private MasterClusters masterClusters;
+
+    @Mock
+    private RegistryLock registryLock;
+
+    /**
+     * Regression test for issue #18197: doMasterFailover used to acquire the 
lock at
+     * {@code /lock/master-failover/<masterAddress>} but release {@code 
/lock/master-failover},
+     * leaking the per-master lock. The fix uses a single lock-path local + 
try-with-resources.
+     */
+    @Test
+    void failoverMaster_releasesTheSameLockItAcquired() {
+        final String masterAddress = "127.0.0.1:5679";
+        final MasterServerMetadata metadata = MasterServerMetadata.builder()
+                .processId(1234)
+                .serverStartupTime(System.currentTimeMillis())
+                .address(masterAddress)
+                .build();
+        final MasterFailoverEvent event = MasterFailoverEvent.of(metadata, new 
Date(), 0L);
+
+        when(clusterManager.getMasterClusters()).thenReturn(masterClusters);
+        
when(masterClusters.getServer(masterAddress)).thenReturn(Optional.empty());
+        when(registryClient.getLock(anyString())).thenReturn(registryLock);
+        when(registryClient.exists(anyString())).thenReturn(false);
+        
when(workflowInstanceDao.queryNeedFailoverWorkflowInstances(masterAddress))
+                .thenReturn(Collections.emptyList());
+
+        failoverCoordinator.failoverMaster(event);
+
+        final ArgumentCaptor<String> lockPathCaptor = 
ArgumentCaptor.forClass(String.class);
+        verify(registryClient).getLock(lockPathCaptor.capture());
+        assertThat(lockPathCaptor.getValue())
+                
.isEqualTo(RegistryUtils.getMasterFailoverLockPath(masterAddress));
+        verify(registryLock, times(1)).close();
+    }
+}
diff --git 
a/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/RegistryClient.java
 
b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/RegistryClient.java
index 3c2606e2c2..d0f2ae0e26 100644
--- 
a/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/RegistryClient.java
+++ 
b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/RegistryClient.java
@@ -190,15 +190,14 @@ public class RegistryClient {
         return registry.exists(key);
     }
 
-    public boolean getLock(String key) {
+    public RegistryLock getLock(String key) {
         if (!registry.isConnected()) {
             throw new IllegalStateException("The registry is not connected");
         }
-        return registry.acquireLock(key);
-    }
-
-    public boolean releaseLock(String key) {
-        return registry.releaseLock(key);
+        if (!registry.acquireLock(key)) {
+            throw new RegistryException("Failed to acquire registry lock: " + 
key);
+        }
+        return new RegistryLock(registry, key);
     }
 
     public void setStoppable(IStoppable stoppable) {
diff --git 
a/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/RegistryLock.java
 
b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/RegistryLock.java
new file mode 100644
index 0000000000..f2d8856c7b
--- /dev/null
+++ 
b/dolphinscheduler-registry/dolphinscheduler-registry-api/src/main/java/org/apache/dolphinscheduler/registry/api/RegistryLock.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.dolphinscheduler.registry.api;
+
+/**
+ * A handle to an acquired registry lock that releases the lock on {@link 
#close()},
+ * intended for use with try-with-resources.
+ * <p>
+ * This handle is bound to the thread that acquired the lock — registry 
backends (e.g.
+ * Zookeeper) keep per-thread state, so the handle must be closed on the same 
thread.
+ * <p>
+ * If {@link #close()} fails (the underlying release throws), the handle is 
left in
+ * an un-closed state so the caller may retry releasing.
+ */
+public final class RegistryLock implements AutoCloseable {
+
+    private final Registry registry;
+    private final String lockKey;
+    private boolean closed = false;
+
+    RegistryLock(Registry registry, String lockKey) {
+        this.registry = registry;
+        this.lockKey = lockKey;
+    }
+
+    public String getLockKey() {
+        return lockKey;
+    }
+
+    @Override
+    public void close() {
+        if (closed) {
+            return;
+        }
+        registry.releaseLock(lockKey);
+        closed = true;
+    }
+}

Reply via email to