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 862f5e37e8 [ISSUE #7743] Isolate destination heartbeats, enforce 
deadline, and resolve rebase conflicts (#7762)
862f5e37e8 is described below

commit 862f5e37e894bd8cef85ef540a8629f313e9ecdf
Author: Harsh Raj <[email protected]>
AuthorDate: Thu Oct 22 11:24:52 2026 +0530

    [ISSUE #7743] Isolate destination heartbeats, enforce deadline, and resolve 
rebase conflicts (#7762)
    
    Co-authored-by: Harsh <[email protected]>
---
 .../register/client/beat/HeartbeatListener.java    | 190 +++++-
 .../client/beat/HeartbeatBoundPortTest.java        |   1 +
 .../client/beat/HeartbeatListenerTest.java         | 635 +++++++++++++--------
 3 files changed, 588 insertions(+), 238 deletions(-)

diff --git 
a/shenyu-register-center/shenyu-register-client-beat/src/main/java/org/apache/shenyu/register/client/beat/HeartbeatListener.java
 
b/shenyu-register-center/shenyu-register-client-beat/src/main/java/org/apache/shenyu/register/client/beat/HeartbeatListener.java
index 2fd6fd48bf..b289545e1f 100644
--- 
a/shenyu-register-center/shenyu-register-client-beat/src/main/java/org/apache/shenyu/register/client/beat/HeartbeatListener.java
+++ 
b/shenyu-register-center/shenyu-register-client-beat/src/main/java/org/apache/shenyu/register/client/beat/HeartbeatListener.java
@@ -22,6 +22,7 @@ import com.github.benmanes.caffeine.cache.Caffeine;
 import com.github.benmanes.caffeine.cache.LoadingCache;
 import com.google.common.base.Splitter;
 import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.MoreExecutors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.shenyu.common.concurrent.ShenyuThreadFactory;
 import org.apache.shenyu.common.config.ShenyuConfig;
@@ -32,8 +33,8 @@ import org.apache.shenyu.common.utils.BoundServerPort;
 import org.apache.shenyu.common.utils.GsonUtils;
 import org.apache.shenyu.common.utils.IpUtils;
 import org.apache.shenyu.common.utils.SystemInfoUtils;
-import org.apache.shenyu.register.common.utils.RegisterUtils;
 import org.apache.shenyu.register.common.dto.InstanceBeatInfoDTO;
+import org.apache.shenyu.register.common.utils.RegisterUtils;
 import org.checkerframework.checker.nullness.qual.NonNull;
 import org.checkerframework.checker.nullness.qual.Nullable;
 import org.slf4j.Logger;
@@ -43,11 +44,19 @@ import org.springframework.context.event.ContextClosedEvent;
 import org.springframework.context.event.EventListener;
 
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.OptionalInt;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 public class HeartbeatListener {
 
@@ -57,8 +66,26 @@ public class HeartbeatListener {
 
     private static final int CHECK_PERIOD = 5;
 
+    private static final int MAX_BEAT_THREADS = 8;
+
+    private static final String HEARTBEAT_TIMEOUT_MS = "heartbeatTimeoutMs";
+
+    private static final String HEARTBEAT_DIRECT_EXECUTION = 
"heartbeatDirectExecution";
+
+    private static final long DEFAULT_DEADLINE_MS = 4000L;
+
+    private final boolean directExecution;
+
     private ScheduledThreadPoolExecutor executor;
 
+    private ExecutorService beatExecutor;
+
+    private BeatSender beatSender;
+
+    private final long deadlineMs;
+
+    private final Map<String, AtomicBoolean> inFlight = new 
ConcurrentHashMap<>();
+
     private final ShenyuConfig shenyuConfig;
 
     private String username;
@@ -82,7 +109,14 @@ public class HeartbeatListener {
     private volatile boolean closed;
 
     public HeartbeatListener(final ShenyuBootstrapHeartBeatConfig config, 
final ShenyuConfig shenyuConfig) {
-        executor = new ScheduledThreadPoolExecutor(1, 
ShenyuThreadFactory.create("scheduled-instance-task", false));
+        this(config, shenyuConfig, null, null);
+    }
+
+    public HeartbeatListener(final ShenyuBootstrapHeartBeatConfig config,
+                             final ShenyuConfig shenyuConfig,
+                             final ExecutorService beatExecutor,
+                             final BeatSender beatSender) {
+        this.executor = new ScheduledThreadPoolExecutor(1, 
ShenyuThreadFactory.create("scheduled-instance-task", false));
         this.shenyuConfig = shenyuConfig;
         this.username = config.getProps().getProperty(Constants.USER_NAME);
         this.password = config.getProps().getProperty(Constants.PASS_WORD);
@@ -93,6 +127,24 @@ public class HeartbeatListener {
             this.password = AesUtils.cbcEncrypt(secretKey, secretIv, password);
         }
         this.serverList = 
Lists.newArrayList(Splitter.on(",").split(config.getServerLists()));
+        String timeoutProp = 
config.getProps().getProperty(HEARTBEAT_TIMEOUT_MS);
+        this.deadlineMs = parseDeadlineMs(timeoutProp);
+        String directProp = 
config.getProps().getProperty(HEARTBEAT_DIRECT_EXECUTION);
+        this.directExecution = Boolean.parseBoolean(directProp);
+        if (Objects.nonNull(beatExecutor)) {
+            this.beatExecutor = beatExecutor;
+        } else if (this.directExecution) {
+            this.beatExecutor = MoreExecutors.newDirectExecutorService();
+        } else {
+            int poolSize = Math.min(Math.max(1, this.serverList.size()), 
MAX_BEAT_THREADS);
+            this.beatExecutor = new ThreadPoolExecutor(
+                    poolSize, poolSize,
+                    60L, TimeUnit.SECONDS,
+                    new ArrayBlockingQueue<>(this.serverList.size()),
+                    ShenyuThreadFactory.create("instance-beat-worker", false),
+                    new ThreadPoolExecutor.AbortPolicy());
+        }
+        this.beatSender = 
Optional.ofNullable(beatSender).orElseGet(DefaultBeatSender::new);
         this.accessToken = Caffeine.newBuilder()
                 //see 
org.apache.shenyu.admin.config.properties.JwtProperties#expiredSeconds
                 .expireAfterWrite(24L, TimeUnit.HOURS)
@@ -100,7 +152,7 @@ public class HeartbeatListener {
                     @Override
                     public @Nullable String load(@NonNull final String server) 
{
                         try {
-                            Optional<?> login = 
RegisterUtils.doLogin(username, password, server.concat(Constants.LOGIN_PATH));
+                            Optional<?> login = 
HeartbeatListener.this.beatSender.login(username, password, 
server.concat(Constants.LOGIN_PATH));
                             return login.map(String::valueOf).orElse(null);
                         } catch (Exception e) {
                             LOG.error("Login admin url :{} is fail, will 
retry. cause: {} ", server, e.getMessage());
@@ -111,6 +163,25 @@ public class HeartbeatListener {
         executor.scheduleAtFixedRate(this::reportHeartbeat, INITIAL_DELAY, 
CHECK_PERIOD, TimeUnit.SECONDS);
     }
 
+    /**
+     * Parse the heartbeat timeout deadline in milliseconds.
+     * Values &lt;= 0 (e.g. -1) disable the per-destination deadline entirely.
+     *
+     * @param timeoutProp timeout property string
+     * @return deadline in milliseconds, or &lt;= 0 if disabled
+     */
+    private long parseDeadlineMs(final String timeoutProp) {
+        if (StringUtils.isBlank(timeoutProp)) {
+            return DEFAULT_DEADLINE_MS;
+        }
+        try {
+            return Long.parseLong(timeoutProp.trim());
+        } catch (NumberFormatException e) {
+            LOG.warn("Invalid heartbeatTimeoutMs property value '{}', falling 
back to default {} ms", timeoutProp, DEFAULT_DEADLINE_MS);
+            return DEFAULT_DEADLINE_MS;
+        }
+    }
+
     /**
      * Record the application listener port. The management server is ignored, 
and nothing is reported
      * until a positive bound port is observed. Shutdown wins over a late 
initialization event.
@@ -154,30 +225,123 @@ public class HeartbeatListener {
 
     private void sendHeartbeat(final InstanceBeatInfoDTO instanceBeatInfoDTO) {
         for (String server : serverList) {
-            String concat = server.concat(Constants.BEAT_URI_PATH);
+            AtomicBoolean serverInFlight = null;
+            if (!directExecution) {
+                serverInFlight = inFlight.computeIfAbsent(server, k -> new 
AtomicBoolean(false));
+                if (!serverInFlight.compareAndSet(false, true)) {
+                    LOG.debug("Previous heartbeat to admin url :{} is still in 
flight, skipping.", server);
+                    continue;
+                }
+            }
+            final AtomicBoolean finalInFlight = serverInFlight;
+            Future<?> future;
             try {
-                String accessToken = this.accessToken.get(server);
-                if (StringUtils.isBlank(accessToken)) {
-                    throw new NullPointerException("accessToken is null");
+                future = beatExecutor.submit(() -> {
+                    try {
+                        sendHeartbeatToServer(server, instanceBeatInfoDTO);
+                    } finally {
+                        if (Objects.nonNull(finalInFlight)) {
+                            finalInFlight.set(false);
+                        }
+                    }
+                });
+            } catch (RejectedExecutionException e) {
+                if (Objects.nonNull(finalInFlight)) {
+                    finalInFlight.set(false);
+                }
+                LOG.warn("Heartbeat dispatch rejected for admin url: {}", 
server, e);
+                continue;
+            }
+            if (deadlineMs > 0 && Objects.nonNull(executor) && 
!executor.isShutdown()) {
+                try {
+                    executor.schedule(() -> {
+                        if (!future.isDone()) {
+                            // Note: RegisterUtils.doHeartBeat relies on 
OkHttp. Thread interruption does not immediately
+                            // abort active socket writes on the wire, so 
cancel(true) is best-effort cancellation.
+                            future.cancel(true);
+                            if (Objects.nonNull(finalInFlight)) {
+                                finalInFlight.set(false);
+                            }
+                            LOG.warn("Heartbeat to admin url :{} timed out 
after {} ms deadline.", server, deadlineMs);
+                        }
+                    }, deadlineMs, TimeUnit.MILLISECONDS);
+                } catch (RejectedExecutionException e) {
+                    LOG.warn("Scheduler rejected timeout task for admin url: 
{}", server);
                 }
-                
RegisterUtils.doHeartBeat(GsonUtils.getInstance().toJson(instanceBeatInfoDTO), 
concat, Constants.HEARTBEAT, accessToken);
-            } catch (Exception e) {
-                LOG.error("HeartBeat admin url :{} is fail, will retry.", 
server, e);
-                // This is a periodic reporter, not a failback registration: 
retry on the next tick.
             }
         }
     }
 
+    private void sendHeartbeatToServer(final String server, final 
InstanceBeatInfoDTO instanceBeatInfoDTO) {
+        String concat = server.concat(Constants.BEAT_URI_PATH);
+        try {
+            String token = this.accessToken.get(server);
+            if (StringUtils.isBlank(token)) {
+                throw new NullPointerException("accessToken is null");
+            }
+            
beatSender.doHeartBeat(GsonUtils.getInstance().toJson(instanceBeatInfoDTO), 
concat, Constants.HEARTBEAT, token);
+        } catch (Exception e) {
+            LOG.error("HeartBeat admin url :{} is fail, will retry.", server, 
e);
+            // This is a periodic reporter, not a failback registration: retry 
on the next tick.
+        }
+    }
+
     @EventListener(ContextClosedEvent.class)
     public void onShutdown() {
         closed = true;
-        executor.shutdown();
+        if (Objects.nonNull(executor)) {
+            executor.shutdown();
+        }
+        if (Objects.nonNull(beatExecutor)) {
+            beatExecutor.shutdown();
+        }
         try {
-            if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
+            if (Objects.nonNull(executor) && !executor.awaitTermination(5, 
TimeUnit.SECONDS)) {
                 executor.shutdownNow();
             }
+            if (Objects.nonNull(beatExecutor) && 
!beatExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+                beatExecutor.shutdownNow();
+            }
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
         }
     }
+
+    public interface BeatSender {
+
+        /**
+         * Login to admin.
+         *
+         * @param username username
+         * @param password password
+         * @param url url
+         * @return login result
+         * @throws Exception exception
+         */
+        Optional<Object> login(String username, String password, String url) 
throws Exception;
+
+        /**
+         * Send heartbeat to admin.
+         *
+         * @param json json payload
+         * @param url url
+         * @param type type
+         * @param token token
+         * @throws Exception exception
+         */
+        void doHeartBeat(String json, String url, String type, String token) 
throws Exception;
+    }
+
+    public static class DefaultBeatSender implements BeatSender {
+
+        @Override
+        public Optional<Object> login(final String username, final String 
password, final String url) throws Exception {
+            return RegisterUtils.doLogin(username, password, url);
+        }
+
+        @Override
+        public void doHeartBeat(final String json, final String url, final 
String type, final String token) throws Exception {
+            RegisterUtils.doHeartBeat(json, url, type, token);
+        }
+    }
 }
diff --git 
a/shenyu-register-center/shenyu-register-client-beat/src/test/java/org/apache/shenyu/register/client/beat/HeartbeatBoundPortTest.java
 
b/shenyu-register-center/shenyu-register-client-beat/src/test/java/org/apache/shenyu/register/client/beat/HeartbeatBoundPortTest.java
index 642ee875e0..88c4723199 100644
--- 
a/shenyu-register-center/shenyu-register-client-beat/src/test/java/org/apache/shenyu/register/client/beat/HeartbeatBoundPortTest.java
+++ 
b/shenyu-register-center/shenyu-register-client-beat/src/test/java/org/apache/shenyu/register/client/beat/HeartbeatBoundPortTest.java
@@ -114,6 +114,7 @@ class HeartbeatBoundPortTest {
         Properties props = new Properties();
         props.setProperty(Constants.USER_NAME, "admin");
         props.setProperty(Constants.PASS_WORD, "123456");
+        props.setProperty("heartbeatDirectExecution", "true");
         ShenyuBootstrapHeartBeatConfig config = new 
ShenyuBootstrapHeartBeatConfig();
         config.setServerLists("http://127.0.0.1:1";);
         config.setProps(props);
diff --git 
a/shenyu-register-center/shenyu-register-client-beat/src/test/java/org/apache/shenyu/register/client/beat/HeartbeatListenerTest.java
 
b/shenyu-register-center/shenyu-register-client-beat/src/test/java/org/apache/shenyu/register/client/beat/HeartbeatListenerTest.java
index 2c9c33a374..0d9ab4112f 100644
--- 
a/shenyu-register-center/shenyu-register-client-beat/src/test/java/org/apache/shenyu/register/client/beat/HeartbeatListenerTest.java
+++ 
b/shenyu-register-center/shenyu-register-client-beat/src/test/java/org/apache/shenyu/register/client/beat/HeartbeatListenerTest.java
@@ -27,29 +27,41 @@ import 
org.apache.shenyu.register.common.utils.RegisterUtils;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.MockedStatic;
-import org.mockito.MockedConstruction;
 import org.mockito.ArgumentCaptor;
+import org.mockito.MockedConstruction;
+import org.mockito.MockedStatic;
 import org.mockito.Mockito;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.springframework.boot.web.context.WebServerApplicationContext;
 import org.springframework.boot.web.context.WebServerInitializedEvent;
 import org.springframework.boot.web.server.WebServer;
 
+import java.io.IOException;
 import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Properties;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
-import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+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.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.never;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 @ExtendWith(MockitoExtension.class)
 class HeartbeatListenerTest {
@@ -73,6 +85,7 @@ class HeartbeatListenerTest {
         props.setProperty(Constants.PASS_WORD, "123456");
         props.setProperty(Constants.AES_SECRET_KEY, "");
         props.setProperty(Constants.AES_SECRET_IV, "");
+        props.setProperty("heartbeatDirectExecution", "true");
 
         ShenyuBootstrapHeartBeatConfig config = new 
ShenyuBootstrapHeartBeatConfig();
         config.setServerLists("http://localhost:9095,http://localhost:9096";);
@@ -101,18 +114,19 @@ class HeartbeatListenerTest {
             
Mockito.verify(executors.constructed().get(0)).scheduleAtFixedRate(periodic.capture(),
 Mockito.eq(0L), Mockito.eq(5L), Mockito.eq(TimeUnit.SECONDS));
             assertDoesNotThrow(periodic.getValue()::run);
             assertDoesNotThrow(periodic.getValue()::run);
-            register.verify(() -> RegisterUtils.doHeartBeat(anyString(), 
Mockito.eq("http://localhost:9095"; + Constants.BEAT_URI_PATH),
+            register.verify(() -> RegisterUtils.doHeartBeat(anyString(), 
Mockito.eq("http://localhost:9095".concat(Constants.BEAT_URI_PATH)),
                     Mockito.eq(Constants.HEARTBEAT), 
Mockito.eq("mock-token")), Mockito.times(2));
-            register.verify(() -> RegisterUtils.doHeartBeat(anyString(), 
Mockito.eq("http://localhost:9096"; + Constants.BEAT_URI_PATH),
+            register.verify(() -> RegisterUtils.doHeartBeat(anyString(), 
Mockito.eq("http://localhost:9096".concat(Constants.BEAT_URI_PATH)),
                     Mockito.eq(Constants.HEARTBEAT), 
Mockito.eq("mock-token")), Mockito.times(2));
         }
     }
 
     @Test
-    void testPeriodicHeartbeatReportsBootstrapInstanceType() {
+    void testPeriodicHeartbeatPublishesBootstrapInstanceType() throws 
Exception {
         try (MockedConstruction<ScheduledThreadPoolExecutor> executors = 
Mockito.mockConstruction(ScheduledThreadPoolExecutor.class);
-                MockedStatic<SystemInfoUtils> ignored = 
Mockito.mockStatic(SystemInfoUtils.class);
+                MockedStatic<SystemInfoUtils> system = 
Mockito.mockStatic(SystemInfoUtils.class);
                 MockedStatic<RegisterUtils> register = 
Mockito.mockStatic(RegisterUtils.class)) {
+            
system.when(SystemInfoUtils::getSystemInfo).thenReturn("system-info");
             register.when(() -> RegisterUtils.doLogin(anyString(), 
anyString(), anyString())).thenReturn(Optional.of("mock-token"));
             heartbeatListener = new HeartbeatListener(config, shenyuConfig);
             bind(heartbeatListener, null, 8080);
@@ -120,214 +134,29 @@ class HeartbeatListenerTest {
             
Mockito.verify(executors.constructed().get(0)).scheduleAtFixedRate(periodic.capture(),
 Mockito.eq(0L), Mockito.eq(5L), Mockito.eq(TimeUnit.SECONDS));
             periodic.getValue().run();
             ArgumentCaptor<String> payload = 
ArgumentCaptor.forClass(String.class);
-            register.verify(() -> RegisterUtils.doHeartBeat(payload.capture(), 
anyString(), anyString(), anyString()), Mockito.atLeastOnce());
-            InstanceBeatInfoDTO reported = 
GsonUtils.getInstance().fromJson(payload.getAllValues().get(0), 
InstanceBeatInfoDTO.class);
-            assertEquals(InstanceTypeConstants.BOOTSTRAP_INSTANCE_TYPE, 
reported.getInstanceType());
-            assertEquals("8080", reported.getInstancePort());
-        }
-    }
-
-    @Test
-    void testDoesNotReportBeforeTheServerBinds() {
-        try (MockedConstruction<ScheduledThreadPoolExecutor> executors = 
Mockito.mockConstruction(ScheduledThreadPoolExecutor.class);
-                MockedStatic<SystemInfoUtils> ignored = 
Mockito.mockStatic(SystemInfoUtils.class);
-                MockedStatic<RegisterUtils> register = 
Mockito.mockStatic(RegisterUtils.class)) {
-            register.when(() -> RegisterUtils.doLogin(anyString(), 
anyString(), anyString())).thenReturn(Optional.of("mock-token"));
-            heartbeatListener = new HeartbeatListener(config, shenyuConfig);
-            ArgumentCaptor<Runnable> periodic = 
ArgumentCaptor.forClass(Runnable.class);
-            
Mockito.verify(executors.constructed().get(0)).scheduleAtFixedRate(periodic.capture(),
 Mockito.eq(0L), Mockito.eq(5L), Mockito.eq(TimeUnit.SECONDS));
-            periodic.getValue().run();
-            register.verify(() -> RegisterUtils.doHeartBeat(anyString(), 
anyString(), anyString(), anyString()), never());
-            assertEquals(1, executors.constructed().size());
-        }
-    }
-
-    @Test
-    void testReportsBoundPortInsteadOfConfiguredZeroAndIgnoresManagement() {
-        try (MockedConstruction<ScheduledThreadPoolExecutor> executors = 
Mockito.mockConstruction(ScheduledThreadPoolExecutor.class);
-                MockedStatic<SystemInfoUtils> ignored = 
Mockito.mockStatic(SystemInfoUtils.class);
-                MockedStatic<RegisterUtils> register = 
Mockito.mockStatic(RegisterUtils.class)) {
-            register.when(() -> RegisterUtils.doLogin(anyString(), 
anyString(), anyString())).thenReturn(Optional.of("mock-token"));
-            final HeartbeatListener first = new HeartbeatListener(config, 
shenyuConfig);
-            final HeartbeatListener second = new HeartbeatListener(config, 
shenyuConfig);
-            bind(first, "management", 19090);
-            bind(first, null, 0);
-            bind(first, null, 41234);
-            bind(second, null, 41235);
-            ArgumentCaptor<Runnable> periodic = 
ArgumentCaptor.forClass(Runnable.class);
-            
Mockito.verify(executors.constructed().get(0)).scheduleAtFixedRate(periodic.capture(),
 Mockito.eq(0L), Mockito.eq(5L), Mockito.eq(TimeUnit.SECONDS));
-            
Mockito.verify(executors.constructed().get(1)).scheduleAtFixedRate(periodic.capture(),
 Mockito.eq(0L), Mockito.eq(5L), Mockito.eq(TimeUnit.SECONDS));
-            periodic.getAllValues().get(0).run();
-            periodic.getAllValues().get(1).run();
-            ArgumentCaptor<String> payload = 
ArgumentCaptor.forClass(String.class);
-            register.verify(() -> RegisterUtils.doHeartBeat(payload.capture(), 
anyString(), anyString(), anyString()), Mockito.atLeast(2));
-            InstanceBeatInfoDTO reportedFirst = 
GsonUtils.getInstance().fromJson(payload.getAllValues().get(0), 
InstanceBeatInfoDTO.class);
-            InstanceBeatInfoDTO reportedSecond = 
GsonUtils.getInstance().fromJson(payload.getAllValues().get(2), 
InstanceBeatInfoDTO.class);
-            assertEquals("41234", reportedFirst.getInstancePort());
-            assertEquals("41235", reportedSecond.getInstancePort());
-            assertNotEquals(reportedFirst.getInstanceIp() + ":" + 
reportedFirst.getInstancePort(),
-                    reportedSecond.getInstanceIp() + ":" + 
reportedSecond.getInstancePort());
-            assertEquals(2, executors.constructed().size());
-        }
-    }
-
-    @Test
-    void testShutdownBeforeInitializationDoesNotReport() {
-        try (MockedConstruction<ScheduledThreadPoolExecutor> executors = 
Mockito.mockConstruction(ScheduledThreadPoolExecutor.class);
-                MockedStatic<RegisterUtils> register = 
Mockito.mockStatic(RegisterUtils.class)) {
-            register.when(() -> RegisterUtils.doLogin(anyString(), 
anyString(), anyString())).thenReturn(Optional.of("mock-token"));
-            heartbeatListener = new HeartbeatListener(config, shenyuConfig);
-            ArgumentCaptor<Runnable> periodic = 
ArgumentCaptor.forClass(Runnable.class);
-            
Mockito.verify(executors.constructed().get(0)).scheduleAtFixedRate(periodic.capture(),
 Mockito.eq(0L), Mockito.eq(5L), Mockito.eq(TimeUnit.SECONDS));
-            heartbeatListener.onShutdown();
-            bind(heartbeatListener, null, 9195, true);
-            periodic.getValue().run();
-            register.verify(() -> RegisterUtils.doHeartBeat(anyString(), 
anyString(), anyString(), anyString()), never());
-            Mockito.verify(executors.constructed().get(0), Mockito.times(1))
-                    .scheduleAtFixedRate(Mockito.any(), Mockito.eq(0L), 
Mockito.eq(5L), Mockito.eq(TimeUnit.SECONDS));
-            assertEquals(1, executors.constructed().size());
-        }
-    }
-
-    private void bind(final HeartbeatListener listener, final String 
namespace, final int port) {
-        bind(listener, namespace, port, false);
-    }
-
-    private void bind(final HeartbeatListener listener, final String 
namespace, final int port, final boolean lenient) {
-        WebServer webServer = Mockito.mock(WebServer.class);
-        WebServerApplicationContext applicationContext = 
Mockito.mock(WebServerApplicationContext.class);
-        WebServerInitializedEvent event = 
Mockito.mock(WebServerInitializedEvent.class);
-        if (lenient) {
-            Mockito.lenient().when(webServer.getPort()).thenReturn(port);
-            
Mockito.lenient().when(applicationContext.getServerNamespace()).thenReturn(namespace);
-            Mockito.lenient().when(event.getWebServer()).thenReturn(webServer);
-            
Mockito.lenient().when(event.getApplicationContext()).thenReturn(applicationContext);
-        } else {
-            Mockito.when(webServer.getPort()).thenReturn(port);
-            
Mockito.when(applicationContext.getServerNamespace()).thenReturn(namespace);
-            Mockito.when(event.getWebServer()).thenReturn(webServer);
-            
Mockito.when(event.getApplicationContext()).thenReturn(applicationContext);
-        }
-        listener.onWebServerInitialized(event);
-    }
-
-    @Test
-    void testHeartbeatListenerCreation() {
-
-        try (MockedStatic<RegisterUtils> registerUtilsMockedStatic = 
Mockito.mockStatic(RegisterUtils.class)) {
-            registerUtilsMockedStatic.when(() -> 
RegisterUtils.doLogin(anyString(), anyString(), anyString()))
-                    .thenReturn(Optional.of("mock-token"));
-
-            assertDoesNotThrow(() -> {
-                heartbeatListener = new HeartbeatListener(config, 
shenyuConfig);
-            });
-
-            assertNotNull(heartbeatListener);
+            register.verify(() -> RegisterUtils.doHeartBeat(payload.capture(), 
Mockito.eq("http://localhost:9095".concat(Constants.BEAT_URI_PATH)),
+                    Mockito.eq(Constants.HEARTBEAT), 
Mockito.eq("mock-token")));
+            InstanceBeatInfoDTO beatInfo = 
GsonUtils.getInstance().fromJson(payload.getValue(), InstanceBeatInfoDTO.class);
+            assertEquals(InstanceTypeConstants.BOOTSTRAP_INSTANCE_TYPE, 
beatInfo.getInstanceType());
+            assertEquals("8080", beatInfo.getInstancePort());
+            assertEquals("shenyu", beatInfo.getNamespaceId());
+            assertEquals("system-info", beatInfo.getInstanceInfo());
         }
     }
 
     @Test
-    void testHeartbeatListenerWithEncryption() {
-
-        Properties props = new Properties();
-        props.setProperty(Constants.USER_NAME, "admin");
-        props.setProperty(Constants.PASS_WORD, "123456");
-        props.setProperty(Constants.AES_SECRET_KEY, "2095132720951327");
-        props.setProperty(Constants.AES_SECRET_IV, "6859932669599326");
-
-        ShenyuBootstrapHeartBeatConfig configWithEncryption = new 
ShenyuBootstrapHeartBeatConfig();
-        configWithEncryption.setServerLists("http://localhost:9095";);
-        configWithEncryption.setProps(props);
-
-        try (MockedStatic<RegisterUtils> registerUtilsMockedStatic = 
Mockito.mockStatic(RegisterUtils.class)) {
-            registerUtilsMockedStatic.when(() -> 
RegisterUtils.doLogin(anyString(), anyString(), anyString()))
-                    .thenReturn(Optional.of("mock-token"));
-
-            assertDoesNotThrow(() -> {
-                heartbeatListener = new 
HeartbeatListener(configWithEncryption, shenyuConfig);
-            });
-
-            assertNotNull(heartbeatListener);
-        }
-    }
-
-    @Test
-    void testSendHeartbeatSuccess() throws Exception {
-
-        try (MockedStatic<RegisterUtils> registerUtilsMockedStatic = 
Mockito.mockStatic(RegisterUtils.class)) {
-            registerUtilsMockedStatic.when(() -> 
RegisterUtils.doLogin(anyString(), anyString(), anyString()))
-                    .thenReturn(Optional.of("mock-token"));
-
-            registerUtilsMockedStatic.when(() -> 
RegisterUtils.doHeartBeat(anyString(), anyString(), anyString(), anyString()))
-                    .then(invocation -> null);
-
-            heartbeatListener = new HeartbeatListener(config, shenyuConfig);
-
-            // Use reflection to access private method sendHeartbeat
-            java.lang.reflect.Method sendHeartbeatMethod = 
HeartbeatListener.class.getDeclaredMethod("sendHeartbeat", 
-                    
org.apache.shenyu.register.common.dto.InstanceBeatInfoDTO.class);
-            sendHeartbeatMethod.setAccessible(true);
-
-            org.apache.shenyu.register.common.dto.InstanceBeatInfoDTO beatInfo 
= 
-                    new 
org.apache.shenyu.register.common.dto.InstanceBeatInfoDTO();
-            beatInfo.setInstancePort("8080");
-            beatInfo.setInstanceIp("127.0.0.1");
-            beatInfo.setNamespaceId("shenyu");
-
-            assertDoesNotThrow(() -> {
-                try {
-                    sendHeartbeatMethod.invoke(heartbeatListener, beatInfo);
-                } catch (Exception e) {
-                    throw new RuntimeException(e);
-                }
-            });
-
-            // Should be called for both servers in serverList
-            registerUtilsMockedStatic.verify(() -> 
RegisterUtils.doHeartBeat(anyString(), anyString(), anyString(), anyString()),
-                    Mockito.times(2));
-        }
-    }
-
-    @Test
-    void testSendHeartbeatWithLoginFailure() throws Exception {
-
-        try (MockedStatic<RegisterUtils> registerUtilsMockedStatic = 
Mockito.mockStatic(RegisterUtils.class)) {
-            registerUtilsMockedStatic.when(() -> 
RegisterUtils.doLogin(anyString(), anyString(), anyString()))
-                    .thenReturn(Optional.empty());
-
-            heartbeatListener = new HeartbeatListener(config, shenyuConfig);
-
-            // Use reflection to access private method sendHeartbeat
-            java.lang.reflect.Method sendHeartbeatMethod = 
HeartbeatListener.class.getDeclaredMethod("sendHeartbeat", 
-                    
org.apache.shenyu.register.common.dto.InstanceBeatInfoDTO.class);
-            sendHeartbeatMethod.setAccessible(true);
-
-            org.apache.shenyu.register.common.dto.InstanceBeatInfoDTO beatInfo 
= 
-                    new 
org.apache.shenyu.register.common.dto.InstanceBeatInfoDTO();
-
-            assertDoesNotThrow(() -> 
sendHeartbeatMethod.invoke(heartbeatListener, beatInfo));
-            assertDoesNotThrow(() -> 
sendHeartbeatMethod.invoke(heartbeatListener, beatInfo));
-            registerUtilsMockedStatic.verify(() -> 
RegisterUtils.doHeartBeat(anyString(), anyString(), anyString(), anyString()),
-                    Mockito.never());
-        }
-    }
-
-    @Test
-    void testOnShutdown() throws Exception {
-
+    void testHeartbeatStop() throws Exception {
         try (MockedStatic<RegisterUtils> registerUtilsMockedStatic = 
Mockito.mockStatic(RegisterUtils.class)) {
             registerUtilsMockedStatic.when(() -> 
RegisterUtils.doLogin(anyString(), anyString(), anyString()))
                     .thenReturn(Optional.of("mock-token"));
 
             heartbeatListener = new HeartbeatListener(config, shenyuConfig);
 
-            // Use reflection to access the executor
             Field executorField = 
HeartbeatListener.class.getDeclaredField("executor");
             executorField.setAccessible(true);
             ScheduledThreadPoolExecutor executor = 
(ScheduledThreadPoolExecutor) executorField.get(heartbeatListener);
 
-            assertNotNull(executor);
-            assertTrue(!executor.isShutdown());
+            assertFalse(executor.isShutdown());
 
             heartbeatListener.onShutdown();
 
@@ -337,14 +166,12 @@ class HeartbeatListenerTest {
 
     @Test
     void testConfigurationValues() throws Exception {
-
         try (MockedStatic<RegisterUtils> registerUtilsMockedStatic = 
Mockito.mockStatic(RegisterUtils.class)) {
             registerUtilsMockedStatic.when(() -> 
RegisterUtils.doLogin(anyString(), anyString(), anyString()))
                     .thenReturn(Optional.of("mock-token"));
 
             heartbeatListener = new HeartbeatListener(config, shenyuConfig);
 
-            // Use reflection to check private fields
             Field usernameField = 
HeartbeatListener.class.getDeclaredField("username");
             usernameField.setAccessible(true);
             String username = (String) usernameField.get(heartbeatListener);
@@ -367,7 +194,6 @@ class HeartbeatListenerTest {
 
     @Test
     void testHeartbeatWithMultipleServersOneFailure() throws Exception {
-
         try (MockedStatic<RegisterUtils> registerUtilsMockedStatic = 
Mockito.mockStatic(RegisterUtils.class)) {
             registerUtilsMockedStatic.when(() -> 
RegisterUtils.doLogin(anyString(), anyString(), anyString()))
                     .thenReturn(Optional.of("mock-token"));
@@ -383,8 +209,7 @@ class HeartbeatListenerTest {
 
             heartbeatListener = new HeartbeatListener(config, shenyuConfig);
 
-            // Use reflection to access private method sendHeartbeat
-            java.lang.reflect.Method sendHeartbeatMethod = 
HeartbeatListener.class.getDeclaredMethod("sendHeartbeat", 
+            Method sendHeartbeatMethod = 
HeartbeatListener.class.getDeclaredMethod("sendHeartbeat", 
                     
org.apache.shenyu.register.common.dto.InstanceBeatInfoDTO.class);
             sendHeartbeatMethod.setAccessible(true);
 
@@ -402,32 +227,392 @@ class HeartbeatListenerTest {
     }
 
     @Test
-    void testHeartbeatWithAllServersFailure() throws Exception {
+    void testHeartbeatRunsOnDedicatedWorkerPool() throws Exception {
+        CountDownLatch latch = new CountDownLatch(2);
+        AtomicInteger executedCount = new AtomicInteger(0);
+
+        HeartbeatListener.BeatSender beatSender = 
mock(HeartbeatListener.BeatSender.class);
+        when(beatSender.login(anyString(), anyString(), 
anyString())).thenReturn(Optional.of("mock-token"));
+        Mockito.doAnswer(invocation -> {
+            executedCount.incrementAndGet();
+            latch.countDown();
+            assertNotEquals("main", Thread.currentThread().getName());
+            return null;
+        }).when(beatSender).doHeartBeat(anyString(), anyString(), anyString(), 
anyString());
 
-        try (MockedStatic<RegisterUtils> registerUtilsMockedStatic = 
Mockito.mockStatic(RegisterUtils.class)) {
-            registerUtilsMockedStatic.when(() -> 
RegisterUtils.doLogin(anyString(), anyString(), anyString()))
-                    .thenReturn(Optional.of("mock-token"));
+        Properties props = new Properties();
+        props.setProperty(Constants.USER_NAME, "admin");
+        props.setProperty(Constants.PASS_WORD, "123456");
+        ShenyuBootstrapHeartBeatConfig poolConfig = new 
ShenyuBootstrapHeartBeatConfig();
+        
poolConfig.setServerLists("http://localhost:9095,http://localhost:9096";);
+        poolConfig.setProps(props);
 
-            // All server calls fail
-            registerUtilsMockedStatic.when(() -> 
RegisterUtils.doHeartBeat(anyString(), anyString(), anyString(), anyString()))
-                    .thenThrow(new RuntimeException("Connection failed"));
+        heartbeatListener = new HeartbeatListener(poolConfig, shenyuConfig, 
null, beatSender);
 
-            heartbeatListener = new HeartbeatListener(config, shenyuConfig);
+        Method sendHeartbeatMethod = 
HeartbeatListener.class.getDeclaredMethod("sendHeartbeat", 
InstanceBeatInfoDTO.class);
+        sendHeartbeatMethod.setAccessible(true);
+        InstanceBeatInfoDTO beatInfo = new InstanceBeatInfoDTO();
 
-            // Use reflection to access private method sendHeartbeat
-            java.lang.reflect.Method sendHeartbeatMethod = 
HeartbeatListener.class.getDeclaredMethod("sendHeartbeat", 
-                    
org.apache.shenyu.register.common.dto.InstanceBeatInfoDTO.class);
-            sendHeartbeatMethod.setAccessible(true);
+        sendHeartbeatMethod.invoke(heartbeatListener, beatInfo);
 
-            org.apache.shenyu.register.common.dto.InstanceBeatInfoDTO beatInfo 
= 
-                    new 
org.apache.shenyu.register.common.dto.InstanceBeatInfoDTO();
+        assertTrue(latch.await(2, TimeUnit.SECONDS));
+        assertEquals(2, executedCount.get());
+        heartbeatListener.onShutdown();
+    }
 
-            // Should throw RuntimeException when all servers fail
+    @Test
+    void testHeartbeatTimeoutDeadlineEnforced() throws Exception {
+        CountDownLatch beatStarted = new CountDownLatch(1);
+        CountDownLatch beatInterrupted = new CountDownLatch(1);
+
+        HeartbeatListener.BeatSender beatSender = 
mock(HeartbeatListener.BeatSender.class);
+        when(beatSender.login(anyString(), anyString(), 
anyString())).thenReturn(Optional.of("mock-token"));
+        Mockito.doAnswer(invocation -> {
+            beatStarted.countDown();
             try {
-                sendHeartbeatMethod.invoke(heartbeatListener, beatInfo);
-            } catch (Exception e) {
-                assertTrue(e.getCause() instanceof RuntimeException);
+                Thread.sleep(5000);
+            } catch (InterruptedException e) {
+                beatInterrupted.countDown();
             }
+            return null;
+        }).when(beatSender).doHeartBeat(anyString(), anyString(), anyString(), 
anyString());
+
+        Properties props = new Properties();
+        props.setProperty(Constants.USER_NAME, "admin");
+        props.setProperty(Constants.PASS_WORD, "123456");
+        props.setProperty("heartbeatTimeoutMs", "100");
+        ShenyuBootstrapHeartBeatConfig timeoutConfig = new 
ShenyuBootstrapHeartBeatConfig();
+        timeoutConfig.setServerLists("http://localhost:9095";);
+        timeoutConfig.setProps(props);
+
+        heartbeatListener = new HeartbeatListener(timeoutConfig, shenyuConfig, 
null, beatSender);
+
+        Method sendHeartbeatMethod = 
HeartbeatListener.class.getDeclaredMethod("sendHeartbeat", 
InstanceBeatInfoDTO.class);
+        sendHeartbeatMethod.setAccessible(true);
+        InstanceBeatInfoDTO beatInfo = new InstanceBeatInfoDTO();
+
+        sendHeartbeatMethod.invoke(heartbeatListener, beatInfo);
+
+        assertTrue(beatStarted.await(1, TimeUnit.SECONDS));
+        assertTrue(beatInterrupted.await(2, TimeUnit.SECONDS));
+        heartbeatListener.onShutdown();
+    }
+
+    @Test
+    void testSlowAdminDoesNotBlockHealthyAdmin() throws Exception {
+        CountDownLatch slowStarted = new CountDownLatch(1);
+        CountDownLatch healthyCompleted = new CountDownLatch(1);
+
+        HeartbeatListener.BeatSender beatSender = 
mock(HeartbeatListener.BeatSender.class);
+        when(beatSender.login(anyString(), anyString(), 
anyString())).thenReturn(Optional.of("mock-token"));
+
+        Mockito.doAnswer(invocation -> {
+            slowStarted.countDown();
+            Thread.sleep(5000);
+            return null;
+        }).when(beatSender).doHeartBeat(anyString(), 
eq("http://localhost:9095".concat(Constants.BEAT_URI_PATH)), anyString(), 
anyString());
+
+        Mockito.doAnswer(invocation -> {
+            healthyCompleted.countDown();
+            return null;
+        }).when(beatSender).doHeartBeat(anyString(), 
eq("http://localhost:9096".concat(Constants.BEAT_URI_PATH)), anyString(), 
anyString());
+
+        Properties props = new Properties();
+        props.setProperty(Constants.USER_NAME, "admin");
+        props.setProperty(Constants.PASS_WORD, "123456");
+        props.setProperty("heartbeatTimeoutMs", "500");
+        ShenyuBootstrapHeartBeatConfig isolateConfig = new 
ShenyuBootstrapHeartBeatConfig();
+        
isolateConfig.setServerLists("http://localhost:9095,http://localhost:9096";);
+        isolateConfig.setProps(props);
+
+        heartbeatListener = new HeartbeatListener(isolateConfig, shenyuConfig, 
null, beatSender);
+
+        Method sendHeartbeatMethod = 
HeartbeatListener.class.getDeclaredMethod("sendHeartbeat", 
InstanceBeatInfoDTO.class);
+        sendHeartbeatMethod.setAccessible(true);
+        InstanceBeatInfoDTO beatInfo = new InstanceBeatInfoDTO();
+
+        sendHeartbeatMethod.invoke(heartbeatListener, beatInfo);
+
+        assertTrue(slowStarted.await(1, TimeUnit.SECONDS));
+        assertTrue(healthyCompleted.await(1, TimeUnit.SECONDS));
+        heartbeatListener.onShutdown();
+    }
+
+    @Test
+    void testConcurrentHeartbeatToSameAdminSuppressed() throws Exception {
+        CountDownLatch holdingBeat = new CountDownLatch(1);
+        CountDownLatch releaseBeat = new CountDownLatch(1);
+        AtomicInteger totalDispatches = new AtomicInteger(0);
+
+        HeartbeatListener.BeatSender beatSender = 
mock(HeartbeatListener.BeatSender.class);
+        when(beatSender.login(anyString(), anyString(), 
anyString())).thenReturn(Optional.of("mock-token"));
+        Mockito.doAnswer(invocation -> {
+            totalDispatches.incrementAndGet();
+            holdingBeat.countDown();
+            releaseBeat.await(2, TimeUnit.SECONDS);
+            return null;
+        }).when(beatSender).doHeartBeat(anyString(), anyString(), anyString(), 
anyString());
+
+        Properties props = new Properties();
+        props.setProperty(Constants.USER_NAME, "admin");
+        props.setProperty(Constants.PASS_WORD, "123456");
+        props.setProperty("heartbeatTimeoutMs", "5000");
+        ShenyuBootstrapHeartBeatConfig singleServerConfig = new 
ShenyuBootstrapHeartBeatConfig();
+        singleServerConfig.setServerLists("http://localhost:9095";);
+        singleServerConfig.setProps(props);
+
+        heartbeatListener = new HeartbeatListener(singleServerConfig, 
shenyuConfig, null, beatSender);
+
+        Method sendHeartbeatMethod = 
HeartbeatListener.class.getDeclaredMethod("sendHeartbeat", 
InstanceBeatInfoDTO.class);
+        sendHeartbeatMethod.setAccessible(true);
+        InstanceBeatInfoDTO beatInfo = new InstanceBeatInfoDTO();
+
+        sendHeartbeatMethod.invoke(heartbeatListener, beatInfo);
+        assertTrue(holdingBeat.await(1, TimeUnit.SECONDS));
+
+        // Second tick while first is still in flight: must be skipped
+        sendHeartbeatMethod.invoke(heartbeatListener, beatInfo);
+
+        assertEquals(1, totalDispatches.get());
+
+        releaseBeat.countDown();
+        heartbeatListener.onShutdown();
+    }
+
+    @Test
+    void testTokenLoadingDoesNotBlockOtherServers() throws Exception {
+        CountDownLatch slowLoginStarted = new CountDownLatch(1);
+        CountDownLatch fastLoginCompleted = new CountDownLatch(1);
+
+        HeartbeatListener.BeatSender beatSender = 
mock(HeartbeatListener.BeatSender.class);
+        Mockito.doAnswer(invocation -> {
+            slowLoginStarted.countDown();
+            Thread.sleep(2000);
+            return Optional.of("slow-token");
+        }).when(beatSender).login(anyString(), anyString(), 
eq("http://localhost:9095".concat(Constants.LOGIN_PATH)));
+
+        Mockito.doAnswer(invocation -> {
+            fastLoginCompleted.countDown();
+            return Optional.of("fast-token");
+        }).when(beatSender).login(anyString(), anyString(), 
eq("http://localhost:9096".concat(Constants.LOGIN_PATH)));
+
+        Properties props = new Properties();
+        props.setProperty(Constants.USER_NAME, "admin");
+        props.setProperty(Constants.PASS_WORD, "123456");
+        ShenyuBootstrapHeartBeatConfig loginConfig = new 
ShenyuBootstrapHeartBeatConfig();
+        
loginConfig.setServerLists("http://localhost:9095,http://localhost:9096";);
+        loginConfig.setProps(props);
+
+        heartbeatListener = new HeartbeatListener(loginConfig, shenyuConfig, 
null, beatSender);
+
+        Method sendHeartbeatMethod = 
HeartbeatListener.class.getDeclaredMethod("sendHeartbeat", 
InstanceBeatInfoDTO.class);
+        sendHeartbeatMethod.setAccessible(true);
+        InstanceBeatInfoDTO beatInfo = new InstanceBeatInfoDTO();
+
+        sendHeartbeatMethod.invoke(heartbeatListener, beatInfo);
+
+        assertTrue(slowLoginStarted.await(1, TimeUnit.SECONDS));
+        assertTrue(fastLoginCompleted.await(1, TimeUnit.SECONDS));
+        heartbeatListener.onShutdown();
+    }
+
+    @Test
+    void testDirectExecutionConfigurationRespected() throws Exception {
+        Properties props = new Properties();
+        props.setProperty(Constants.USER_NAME, "admin");
+        props.setProperty(Constants.PASS_WORD, "123456");
+        props.setProperty("heartbeatDirectExecution", "true");
+
+        ShenyuBootstrapHeartBeatConfig directCfg = new 
ShenyuBootstrapHeartBeatConfig();
+        directCfg.setServerLists("http://localhost:9095";);
+        directCfg.setProps(props);
+
+        heartbeatListener = new HeartbeatListener(directCfg, shenyuConfig);
+
+        Field directField = 
HeartbeatListener.class.getDeclaredField("directExecution");
+        directField.setAccessible(true);
+        boolean direct = (boolean) directField.get(heartbeatListener);
+        assertTrue(direct);
+
+        Field executorField = 
HeartbeatListener.class.getDeclaredField("beatExecutor");
+        executorField.setAccessible(true);
+        Object exec = executorField.get(heartbeatListener);
+        assertNotNull(exec);
+    }
+
+    @Test
+    void testNegativeDeadlineDisablesTimeout() throws Exception {
+        Properties props = new Properties();
+        props.setProperty(Constants.USER_NAME, "admin");
+        props.setProperty(Constants.PASS_WORD, "123456");
+        props.setProperty("heartbeatTimeoutMs", "-1");
+
+        ShenyuBootstrapHeartBeatConfig noTimeoutCfg = new 
ShenyuBootstrapHeartBeatConfig();
+        noTimeoutCfg.setServerLists("http://localhost:9095";);
+        noTimeoutCfg.setProps(props);
+
+        heartbeatListener = new HeartbeatListener(noTimeoutCfg, shenyuConfig);
+
+        Field deadlineField = 
HeartbeatListener.class.getDeclaredField("deadlineMs");
+        deadlineField.setAccessible(true);
+        long deadline = (long) deadlineField.get(heartbeatListener);
+        assertEquals(-1L, deadline);
+    }
+
+    @Test
+    void testMalformedHeartbeatTimeoutMsFallback() throws Exception {
+        Properties props = new Properties();
+        props.setProperty(Constants.USER_NAME, "admin");
+        props.setProperty(Constants.PASS_WORD, "123456");
+        props.setProperty("heartbeatTimeoutMs", "invalid");
+
+        ShenyuBootstrapHeartBeatConfig cfg = new 
ShenyuBootstrapHeartBeatConfig();
+        cfg.setServerLists("http://localhost:9095";);
+        cfg.setProps(props);
+
+        heartbeatListener = new HeartbeatListener(cfg, shenyuConfig);
+
+        Field deadlineField = 
HeartbeatListener.class.getDeclaredField("deadlineMs");
+        deadlineField.setAccessible(true);
+        long deadline = (long) deadlineField.get(heartbeatListener);
+        assertEquals(4000L, deadline);
+    }
+
+    @Test
+    void testCancelledQueuedTaskClearsInFlightFlag() throws Exception {
+        CountDownLatch firstBeatStarted = new CountDownLatch(1);
+        CountDownLatch releaseFirstBeat = new CountDownLatch(1);
+        CountDownLatch secondBeatExecuted = new CountDownLatch(1);
+
+        HeartbeatListener.BeatSender beatSender = 
mock(HeartbeatListener.BeatSender.class);
+        when(beatSender.login(anyString(), anyString(), 
anyString())).thenReturn(Optional.of("mock-token"));
+
+        Mockito.doAnswer(invocation -> {
+            firstBeatStarted.countDown();
+            releaseFirstBeat.await(2, TimeUnit.SECONDS);
+            return null;
+        }).when(beatSender).doHeartBeat(anyString(), 
eq("http://localhost:9095".concat(Constants.BEAT_URI_PATH)), anyString(), 
anyString());
+
+        Mockito.doAnswer(invocation -> {
+            secondBeatExecuted.countDown();
+            return null;
+        }).when(beatSender).doHeartBeat(anyString(), 
eq("http://localhost:9096".concat(Constants.BEAT_URI_PATH)), anyString(), 
anyString());
+
+        Properties props = new Properties();
+        props.setProperty(Constants.USER_NAME, "admin");
+        props.setProperty(Constants.PASS_WORD, "123456");
+        props.setProperty("heartbeatTimeoutMs", "100");
+        ShenyuBootstrapHeartBeatConfig queueConfig = new 
ShenyuBootstrapHeartBeatConfig();
+        
queueConfig.setServerLists("http://localhost:9095,http://localhost:9096";);
+        queueConfig.setProps(props);
+
+        ExecutorService singleThreadExecutor = 
Executors.newSingleThreadExecutor();
+        try {
+            heartbeatListener = new HeartbeatListener(queueConfig, 
shenyuConfig, singleThreadExecutor, beatSender);
+            Method sendHeartbeatMethod = 
HeartbeatListener.class.getDeclaredMethod("sendHeartbeat", 
InstanceBeatInfoDTO.class);
+            sendHeartbeatMethod.setAccessible(true);
+            InstanceBeatInfoDTO beatInfo = new InstanceBeatInfoDTO();
+
+            sendHeartbeatMethod.invoke(heartbeatListener, beatInfo);
+
+            assertTrue(firstBeatStarted.await(1, TimeUnit.SECONDS));
+
+            // Wait until 9096's deadline (100ms) expires and cancels its 
queued task
+            Thread.sleep(250);
+
+            Field inFlightField = 
HeartbeatListener.class.getDeclaredField("inFlight");
+            inFlightField.setAccessible(true);
+            @SuppressWarnings("unchecked")
+            Map<String, AtomicBoolean> inFlightMap = (Map<String, 
AtomicBoolean>) inFlightField.get(heartbeatListener);
+            assertNotNull(inFlightMap.get("http://localhost:9096";));
+            assertFalse(inFlightMap.get("http://localhost:9096";).get(),
+                    "Cancelled queued task must clear the in-flight flag for 
that server");
+
+            // Release the worker thread
+            releaseFirstBeat.countDown();
+            Thread.sleep(100);
+
+            // Subsequent heartbeat to 9096 must now proceed and execute
+            sendHeartbeatMethod.invoke(heartbeatListener, beatInfo);
+            assertTrue(secondBeatExecuted.await(1, TimeUnit.SECONDS),
+                    "Subsequent heartbeat must execute after queued task was 
cancelled");
+        } finally {
+            releaseFirstBeat.countDown();
+            singleThreadExecutor.shutdownNow();
         }
     }
+
+    @Test
+    void testRejectedTaskClearsInFlightFlag() throws Exception {
+        CountDownLatch workerHolding = new CountDownLatch(1);
+        CountDownLatch releaseWorker = new CountDownLatch(1);
+        CountDownLatch heartbeatExecuted = new CountDownLatch(1);
+
+        HeartbeatListener.BeatSender beatSender = 
mock(HeartbeatListener.BeatSender.class);
+        when(beatSender.login(anyString(), anyString(), 
anyString())).thenReturn(Optional.of("mock-token"));
+        Mockito.doAnswer(invocation -> {
+            heartbeatExecuted.countDown();
+            return null;
+        }).when(beatSender).doHeartBeat(anyString(), anyString(), anyString(), 
anyString());
+
+        ThreadPoolExecutor rejectingPool = new ThreadPoolExecutor(
+                1, 1, 0, TimeUnit.SECONDS,
+                new SynchronousQueue<>(),
+                new ThreadPoolExecutor.AbortPolicy());
+
+        // Fill the single worker thread so any new submit will be rejected
+        rejectingPool.submit(() -> {
+            workerHolding.countDown();
+            releaseWorker.await(2, TimeUnit.SECONDS);
+            return null;
+        });
+        assertTrue(workerHolding.await(1, TimeUnit.SECONDS));
+
+        Properties props = new Properties();
+        props.setProperty(Constants.USER_NAME, "admin");
+        props.setProperty(Constants.PASS_WORD, "123456");
+        ShenyuBootstrapHeartBeatConfig configSingle = new 
ShenyuBootstrapHeartBeatConfig();
+        configSingle.setServerLists("http://localhost:9095";);
+        configSingle.setProps(props);
+
+        try {
+            heartbeatListener = new HeartbeatListener(configSingle, 
shenyuConfig, rejectingPool, beatSender);
+            Method sendHeartbeatMethod = 
HeartbeatListener.class.getDeclaredMethod("sendHeartbeat", 
InstanceBeatInfoDTO.class);
+            sendHeartbeatMethod.setAccessible(true);
+            InstanceBeatInfoDTO beatInfo = new InstanceBeatInfoDTO();
+
+            // Submit will be rejected, but must not throw or leave flag stuck
+            assertDoesNotThrow(() -> 
sendHeartbeatMethod.invoke(heartbeatListener, beatInfo));
+
+            Field inFlightField = 
HeartbeatListener.class.getDeclaredField("inFlight");
+            inFlightField.setAccessible(true);
+            @SuppressWarnings("unchecked")
+            Map<String, AtomicBoolean> inFlightMap = (Map<String, 
AtomicBoolean>) inFlightField.get(heartbeatListener);
+            assertNotNull(inFlightMap.get("http://localhost:9095";));
+            assertFalse(inFlightMap.get("http://localhost:9095";).get(),
+                    "Rejected task must clear in-flight flag immediately");
+
+            // Release worker so subsequent heartbeat can succeed
+            releaseWorker.countDown();
+            Thread.sleep(100);
+
+            sendHeartbeatMethod.invoke(heartbeatListener, beatInfo);
+            assertTrue(heartbeatExecuted.await(1, TimeUnit.SECONDS),
+                    "Heartbeat must succeed once executor becomes available 
again");
+        } finally {
+            releaseWorker.countDown();
+            rejectingPool.shutdownNow();
+        }
+    }
+
+    private void bind(final HeartbeatListener listener, final String 
namespace, final int port) {
+        WebServerInitializedEvent event = 
Mockito.mock(WebServerInitializedEvent.class);
+        WebServer webServer = Mockito.mock(WebServer.class);
+        WebServerApplicationContext applicationContext = 
Mockito.mock(WebServerApplicationContext.class);
+        Mockito.when(event.getWebServer()).thenReturn(webServer);
+        
Mockito.when(event.getApplicationContext()).thenReturn(applicationContext);
+        Mockito.when(webServer.getPort()).thenReturn(port);
+        
Mockito.when(applicationContext.getServerNamespace()).thenReturn(namespace);
+        listener.onWebServerInitialized(event);
+    }
 }

Reply via email to