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

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


The following commit(s) were added to refs/heads/master by this push:
     new bb076aedc0 fix(admin): stop the master selector scheduler on cluster 
shutdown (#7774) (#7782)
bb076aedc0 is described below

commit bb076aedc04e704dcd54493b45aeefbb30d9b0a7
Author: konlue <[email protected]>
AuthorDate: Mon Oct 19 09:33:16 2026 +0800

    fix(admin): stop the master selector scheduler on cluster shutdown (#7774) 
(#7782)
---
 .../mode/cluster/service/ShenyuClusterService.java |  16 ++-
 .../cluster/service/ShenyuClusterServiceTest.java  | 127 +++++++++++++++++++++
 2 files changed, 140 insertions(+), 3 deletions(-)

diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/service/ShenyuClusterService.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/service/ShenyuClusterService.java
index d80de5d5db..6d89c8c5e6 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/service/ShenyuClusterService.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mode/cluster/service/ShenyuClusterService.java
@@ -31,9 +31,11 @@ import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 
 public class ShenyuClusterService implements ShenyuRunningModeService {
-    
+
     private static final Logger LOG = 
LoggerFactory.getLogger(ShenyuClusterService.class);
-    
+
+    private static final long SHUTDOWN_WAIT_SECONDS = 5L;
+
     private final ClusterSelectMasterService shenyuClusterSelectMasterService;
     
     private final UpstreamCheckService upstreamCheckService;
@@ -125,6 +127,14 @@ public class ShenyuClusterService implements 
ShenyuRunningModeService {
     
     @Override
     public void shutdown() {
-    
+        executorService.shutdownNow();
+        try {
+            if (!executorService.awaitTermination(SHUTDOWN_WAIT_SECONDS, 
TimeUnit.SECONDS)) {
+                LOG.warn("master selector scheduler did not terminate within 
{}s", SHUTDOWN_WAIT_SECONDS);
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            LOG.warn("interrupted while waiting for the master selector 
scheduler to terminate");
+        }
     }
 }
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mode/cluster/service/ShenyuClusterServiceTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mode/cluster/service/ShenyuClusterServiceTest.java
new file mode 100644
index 0000000000..25874909b5
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mode/cluster/service/ShenyuClusterServiceTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.shenyu.admin.mode.cluster.service;
+
+import org.apache.shenyu.admin.config.properties.ClusterProperties;
+import org.apache.shenyu.admin.service.impl.InstanceCheckService;
+import org.apache.shenyu.admin.service.impl.UpstreamCheckService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test case for {@link ShenyuClusterService} shutdown lifecycle.
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+public final class ShenyuClusterServiceTest {
+
+    private static final long AWAIT_MILLIS = 5000L;
+
+    @Mock
+    private ClusterSelectMasterService clusterSelectMasterService;
+
+    @Mock
+    private UpstreamCheckService upstreamCheckService;
+
+    @Mock
+    private InstanceCheckService instanceCheckService;
+
+    private ShenyuClusterService clusterService;
+
+    @BeforeEach
+    public void setUp() {
+        final ClusterProperties properties = new ClusterProperties();
+        properties.setSelectPeriod(1L);
+        clusterService = new ShenyuClusterService(clusterSelectMasterService, 
upstreamCheckService, instanceCheckService, properties);
+    }
+
+    @Test
+    public void testShutdownBeforeStartDoesNotThrow() {
+        assertDoesNotThrow(clusterService::shutdown);
+        assertTrue(scheduler().isShutdown());
+    }
+
+    @Test
+    public void testShutdownStopsElectionScheduler() throws Exception {
+        final AtomicInteger attempts = new AtomicInteger();
+        when(clusterSelectMasterService.selectMaster(anyString(), anyString(), 
anyString()))
+                .thenAnswer(invocation -> {
+                    attempts.incrementAndGet();
+                    return false;
+                });
+
+        clusterService.start("127.0.0.1", 9095, "/");
+        awaitAttempts(attempts, 1);
+
+        clusterService.shutdown();
+
+        assertTrue(scheduler().isShutdown());
+        final int afterShutdown = attempts.get();
+        Thread.sleep(2500L);
+        assertEquals(afterShutdown, attempts.get(), "no election attempt may 
happen after completed shutdown");
+
+        assertDoesNotThrow(clusterService::shutdown);
+    }
+
+    @Test
+    public void testShutdownInterruptsRenewalAndReleasesLeadership() throws 
Exception {
+        when(clusterSelectMasterService.selectMaster(anyString(), anyString(), 
anyString())).thenReturn(true);
+        when(clusterSelectMasterService.checkMasterStatus()).thenReturn(true);
+
+        clusterService.start("127.0.0.1", 9095, "/");
+        // the first election succeeded and the renewal loop is running
+        org.mockito.Mockito.verify(clusterSelectMasterService, 
timeout(AWAIT_MILLIS).atLeastOnce())
+                .selectMaster(anyString(), anyString(), anyString());
+
+        clusterService.shutdown();
+
+        // the interrupted renewal loop releases leadership through its own 
finally block
+        org.mockito.Mockito.verify(clusterSelectMasterService, 
timeout(3000L)).releaseMaster();
+        // and the existing error path closes the owned check services
+        org.mockito.Mockito.verify(upstreamCheckService, 
timeout(3000L)).close();
+        org.mockito.Mockito.verify(instanceCheckService, 
timeout(3000L)).close();
+    }
+
+    private ScheduledExecutorService scheduler() {
+        return (ScheduledExecutorService) 
ReflectionTestUtils.getField(clusterService, "executorService");
+    }
+
+    private void awaitAttempts(final AtomicInteger attempts, final int 
expected) throws Exception {
+        final long deadline = System.currentTimeMillis() + AWAIT_MILLIS;
+        while (attempts.get() < expected && System.currentTimeMillis() < 
deadline) {
+            Thread.sleep(50L);
+        }
+        assertTrue(attempts.get() >= expected, "election attempt did not 
happen in time");
+    }
+}

Reply via email to