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 f4bc4afa92 fix(admin): forward instance heartbeats to the master admin 
(#7778) (#7797)
f4bc4afa92 is described below

commit f4bc4afa92d45fe7597c0fa04e4053e151f145dc
Author: konlue <[email protected]>
AuthorDate: Mon Oct 19 09:32:38 2026 +0800

    fix(admin): forward instance heartbeats to the master admin (#7778) (#7797)
---
 shenyu-admin/src/main/resources/application.yml    |   1 +
 .../cluster/filter/ClusterForwardFilterTest.java   | 175 +++++++++++++++++++++
 2 files changed, 176 insertions(+)

diff --git a/shenyu-admin/src/main/resources/application.yml 
b/shenyu-admin/src/main/resources/application.yml
index 69af897f43..9265355509 100755
--- a/shenyu-admin/src/main/resources/application.yml
+++ b/shenyu-admin/src/main/resources/application.yml
@@ -142,6 +142,7 @@ shenyu:
     forward-list:
       - /shenyu-client/**
       - /configs/**
+      - /instance/beat
       - /selector/batchEnabled
       - /selector/batch
       - /selector
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mode/cluster/filter/ClusterForwardFilterTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mode/cluster/filter/ClusterForwardFilterTest.java
new file mode 100644
index 0000000000..0436d20af9
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mode/cluster/filter/ClusterForwardFilterTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.filter;
+
+import org.apache.shenyu.admin.config.properties.ClusterProperties;
+import org.apache.shenyu.admin.model.dto.ClusterMasterDTO;
+import org.apache.shenyu.admin.mode.cluster.service.ClusterSelectMasterService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.mock.web.MockFilterChain;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.client.RestTemplate;
+import org.yaml.snakeyaml.Yaml;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test case for {@link ClusterForwardFilter}.
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+public final class ClusterForwardFilterTest {
+
+    private static final String MASTER_HOST = "10.0.0.9";
+
+    private static final String MASTER_PORT = "9095";
+
+    @Mock
+    private RestTemplate restTemplate;
+
+    @Mock
+    private ClusterSelectMasterService clusterSelectMasterService;
+
+    private ClusterForwardFilter clusterForwardFilter;
+
+    private ClusterProperties clusterProperties;
+
+    @BeforeEach
+    public void setUp() {
+        clusterForwardFilter = new ClusterForwardFilter(restTemplate);
+        clusterProperties = new ClusterProperties();
+        ReflectionTestUtils.setField(clusterForwardFilter, 
"clusterSelectMasterService", clusterSelectMasterService);
+        ReflectionTestUtils.setField(clusterForwardFilter, 
"clusterProperties", clusterProperties);
+
+        final ClusterMasterDTO master = new ClusterMasterDTO();
+        master.setMasterHost(MASTER_HOST);
+        master.setMasterPort(MASTER_PORT);
+        master.setContextPath("");
+        when(clusterSelectMasterService.getMaster()).thenReturn(master);
+    }
+
+    @Test
+    public void testMasterHandlesRequestsLocally() throws Exception {
+        when(clusterSelectMasterService.isMaster()).thenReturn(true);
+
+        final MockHttpServletRequest request = beatRequest();
+        final MockFilterChain filterChain = new MockFilterChain();
+        clusterForwardFilter.doFilter(request, new MockHttpServletResponse(), 
filterChain);
+
+        assertNotNull(filterChain.getRequest(), "master must handle the 
request locally");
+        verify(restTemplate, never()).exchange(anyString(), 
any(HttpMethod.class), any(HttpEntity.class), eq(byte[].class));
+    }
+
+    @Test
+    public void testInstanceBeatForwardedToMaster() throws Exception {
+        when(clusterSelectMasterService.isMaster()).thenReturn(false);
+        
clusterProperties.setForwardList(Collections.singletonList("/instance/beat"));
+
+        final MockHttpServletRequest request = beatRequest();
+        final MockFilterChain filterChain = new MockFilterChain();
+        final MockHttpServletResponse response = new MockHttpServletResponse();
+        when(restTemplate.exchange(anyString(), any(HttpMethod.class), 
any(HttpEntity.class), eq(byte[].class)))
+                .thenReturn(new 
ResponseEntity<>("success".getBytes(StandardCharsets.UTF_8), HttpStatus.OK));
+
+        clusterForwardFilter.doFilter(request, response, filterChain);
+
+        final ArgumentCaptor<String> urlCaptor = 
ArgumentCaptor.forClass(String.class);
+        final ArgumentCaptor<HttpEntity<byte[]>> entityCaptor = 
ArgumentCaptor.forClass(HttpEntity.class);
+        verify(restTemplate).exchange(urlCaptor.capture(), 
eq(HttpMethod.POST), entityCaptor.capture(), eq(byte[].class));
+        assertTrue(urlCaptor.getValue().contains(MASTER_HOST + ":" + 
MASTER_PORT), urlCaptor.getValue());
+        assertTrue(urlCaptor.getValue().contains("/instance/beat"), 
urlCaptor.getValue());
+        assertEquals("{\"instanceIp\":\"127.0.0.1\"}", new 
String(entityCaptor.getValue().getBody(), StandardCharsets.UTF_8));
+        assertEquals("application/json", 
entityCaptor.getValue().getHeaders().getFirst("Content-Type"));
+        assertNull(filterChain.getRequest(), "forwarded requests must not run 
the local chain");
+    }
+
+    @Test
+    public void testUnlistedPathHandledLocally() throws Exception {
+        when(clusterSelectMasterService.isMaster()).thenReturn(false);
+        
clusterProperties.setForwardList(Collections.singletonList("/selector/**"));
+
+        final MockHttpServletRequest request = beatRequest();
+        final MockFilterChain filterChain = new MockFilterChain();
+        clusterForwardFilter.doFilter(request, new MockHttpServletResponse(), 
filterChain);
+
+        assertNotNull(filterChain.getRequest(), "paths outside the forward 
list must be handled locally");
+        verify(restTemplate, never()).exchange(anyString(), 
any(HttpMethod.class), any(HttpEntity.class), eq(byte[].class));
+    }
+
+    @Test
+    public void testApplicationYmlForwardsInstanceBeat() throws Exception {
+        final Yaml yaml = new Yaml();
+        // the main application.yml (the test classpath one is a test fixture 
without the cluster section)
+        try (InputStream in = 
java.nio.file.Files.newInputStream(java.nio.file.Paths.get("src", "main", 
"resources", "application.yml"))) {
+            final Map<String, Object> root = yaml.load(in);
+            final Map<String, Object> shenyu = castMap(root.get("shenyu"));
+            final Map<String, Object> cluster = castMap(shenyu.get("cluster"));
+            final List<String> forwardList = 
castList(cluster.get("forward-list"));
+            assertTrue(forwardList.contains("/instance/beat"),
+                    "instance heartbeats must be forwarded to the master 
admin");
+        }
+    }
+
+    private MockHttpServletRequest beatRequest() {
+        final MockHttpServletRequest request = new MockHttpServletRequest();
+        request.setMethod("POST");
+        request.setRequestURI("/instance/beat");
+        request.setContextPath("");
+        request.setContentType("application/json");
+        
request.setContent("{\"instanceIp\":\"127.0.0.1\"}".getBytes(StandardCharsets.UTF_8));
+        return request;
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<String, Object> castMap(final Object value) {
+        return (Map<String, Object>) value;
+    }
+
+    @SuppressWarnings("unchecked")
+    private List<String> castList(final Object value) {
+        return (List<String>) value;
+    }
+}

Reply via email to