This is an automated email from the ASF dual-hosted git repository.
moremind 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 c8961528b7 Prevent duplicate full-mode URI heartbeats after repeated
context refresh (#6337)
c8961528b7 is described below
commit c8961528b72a2a7f81f0d1cd7525af1659a670bc
Author: aias00 <[email protected]>
AuthorDate: Fri May 8 16:17:42 2026 +0800
Prevent duplicate full-mode URI heartbeats after repeated context refresh
(#6337)
Full-mode client listeners published URI registration before the shared
one-time registration gate ran, so repeated ContextRefreshedEvent
invocations could enqueue the same URI multiple times and amplify
heartbeat logging. This change routes full-mode registration through the
same gate as the standard listener flow and locks the behavior with
Spring MVC and WebSocket regression tests.
Constraint: Full-mode registration must preserve standard one-shot listener
semantics
Rejected: Deduplicate in the heartbeat subscriber | leaves duplicate
registration side effects in place
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep any full-mode registration path behind the shared
registration gate
Tested: mvn -pl
shenyu-client/shenyu-client-core,shenyu-client/shenyu-client-http/shenyu-client-springmvc,shenyu-client/shenyu-client-websocket/shenyu-client-spring-websocket
-am -DfailIfNoTests=false
-Dtest=ShenyuClientURIExecutorSubscriberTest,SpringMvcClientEventListenerTest,SpringWebSocketClientEventListenerTest
test
Not-tested: Live expos-admin-server runtime verification
---
.../AbstractContextRefreshedEventListener.java | 11 ++++-
.../init/SpringMvcClientEventListener.java | 8 ++--
.../init/SpringMvcClientEventListenerTest.java | 16 +++++++
.../init/SpringWebSocketClientEventListener.java | 8 ++--
.../SpringWebSocketClientEventListenerTest.java | 54 +++++++++++++++-------
5 files changed, 72 insertions(+), 25 deletions(-)
diff --git
a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/client/AbstractContextRefreshedEventListener.java
b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/client/AbstractContextRefreshedEventListener.java
index 2e55cc4c59..32ef07fcda 100644
---
a/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/client/AbstractContextRefreshedEventListener.java
+++
b/shenyu-client/shenyu-client-core/src/main/java/org/apache/shenyu/client/core/client/AbstractContextRefreshedEventListener.java
@@ -150,7 +150,7 @@ public abstract class
AbstractContextRefreshedEventListener<T, A extends Annotat
if (MapUtils.isEmpty(beans)) {
return;
}
- if (!registered.compareAndSet(false, true)) {
+ if (!markRegistered()) {
return;
}
if (isDiscoveryLocalMode) {
@@ -372,6 +372,15 @@ public abstract class
AbstractContextRefreshedEventListener<T, A extends Annotat
return publisher;
}
+ /**
+ * Mark the listener as registered once.
+ *
+ * @return true when this invocation acquires the registration gate
+ */
+ protected boolean markRegistered() {
+ return registered.compareAndSet(false, true);
+ }
+
/**
* Get the metadata map.
*
diff --git
a/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/main/java/org/apache/shenyu/client/springmvc/init/SpringMvcClientEventListener.java
b/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/main/java/org/apache/shenyu/client/springmvc/init/SpringMvcClientEventListener.java
index d2f89f8816..17e8f3145c 100644
---
a/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/main/java/org/apache/shenyu/client/springmvc/init/SpringMvcClientEventListener.java
+++
b/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/main/java/org/apache/shenyu/client/springmvc/init/SpringMvcClientEventListener.java
@@ -21,7 +21,6 @@ import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import
org.apache.shenyu.client.core.client.AbstractContextRefreshedEventListener;
import org.apache.shenyu.client.core.constant.ShenyuClientConstants;
-import
org.apache.shenyu.client.core.disruptor.ShenyuClientRegisterEventPublisher;
import org.apache.shenyu.client.core.utils.PortUtils;
import org.apache.shenyu.client.springmvc.annotation.ShenyuSpringMvcClient;
import org.apache.shenyu.common.enums.ApiHttpMethodEnum;
@@ -68,8 +67,6 @@ public class SpringMvcClientEventListener extends
AbstractContextRefreshedEventL
private static final Logger LOG =
LoggerFactory.getLogger(SpringMvcClientEventListener.class);
- private final ShenyuClientRegisterEventPublisher publisher =
ShenyuClientRegisterEventPublisher.getInstance();
-
private final List<Class<? extends Annotation>> mappingAnnotation = new
ArrayList<>(3);
private final Boolean isFull;
@@ -121,6 +118,9 @@ public class SpringMvcClientEventListener extends
AbstractContextRefreshedEventL
protected Map<String, Object> getBeans(final ApplicationContext context) {
// Filter out
if (Boolean.TRUE.equals(isFull)) {
+ if (!markRegistered()) {
+ return Collections.emptyMap();
+ }
LOG.info("init spring mvc client success with isFull mode");
List<String> namespaceIds = super.getNamespace();
namespaceIds.forEach(namespaceId -> {
@@ -135,7 +135,7 @@ public class SpringMvcClientEventListener extends
AbstractContextRefreshedEventL
.namespaceId(namespaceId)
.build());
- publisher.publishEvent(buildURIRegisterDTO(context,
Collections.emptyMap(), namespaceId));
+ getPublisher().publishEvent(buildURIRegisterDTO(context,
Collections.emptyMap(), namespaceId));
});
return Collections.emptyMap();
}
diff --git
a/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/test/java/org/apache/shenyu/client/springmvc/init/SpringMvcClientEventListenerTest.java
b/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/test/java/org/apache/shenyu/client/springmvc/init/SpringMvcClientEventListenerTest.java
index a9e4ba8c49..33ec3f1066 100644
---
a/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/test/java/org/apache/shenyu/client/springmvc/init/SpringMvcClientEventListenerTest.java
+++
b/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/test/java/org/apache/shenyu/client/springmvc/init/SpringMvcClientEventListenerTest.java
@@ -18,6 +18,7 @@
package org.apache.shenyu.client.springmvc.init;
import org.apache.shenyu.client.core.constant.ShenyuClientConstants;
+import
org.apache.shenyu.client.core.disruptor.ShenyuClientRegisterEventPublisher;
import
org.apache.shenyu.client.core.exception.ShenyuClientIllegalArgumentException;
import
org.apache.shenyu.client.core.register.ShenyuClientRegisterRepositoryFactory;
import org.apache.shenyu.client.springmvc.annotation.ShenyuSpringMvcClient;
@@ -178,6 +179,21 @@ public class SpringMvcClientEventListenerTest {
registerUtilsMockedStatic.close();
}
+ @Test
+ public void testOnApplicationEventFullModeShouldRegisterOnce() {
+ try (MockedStatic<ShenyuClientRegisterEventPublisher>
publisherMockedStatic = mockStatic(ShenyuClientRegisterEventPublisher.class)) {
+ ShenyuClientRegisterEventPublisher publisher =
mock(ShenyuClientRegisterEventPublisher.class);
+
publisherMockedStatic.when(ShenyuClientRegisterEventPublisher::getInstance).thenReturn(publisher);
+ SpringMvcClientEventListener springMvcClientEventListener =
buildSpringMvcClientEventListener(true, true);
+ ContextRefreshedEvent event = new
ContextRefreshedEvent(applicationContext);
+ springMvcClientEventListener.onApplicationEvent(event);
+ springMvcClientEventListener.onApplicationEvent(event);
+ verify(publisher, times(1)).start(any());
+ verify(publisher, times(2)).publishEvent(any());
+ }
+ registerUtilsMockedStatic.close();
+ }
+
@Test
public void testOnBuildApiSuperPath() {
SpringMvcClientEventListener springMvcClientEventListener =
buildSpringMvcClientEventListener(false, false);
diff --git
a/shenyu-client/shenyu-client-websocket/shenyu-client-spring-websocket/src/main/java/org/apache/shenyu/client/spring/websocket/init/SpringWebSocketClientEventListener.java
b/shenyu-client/shenyu-client-websocket/shenyu-client-spring-websocket/src/main/java/org/apache/shenyu/client/spring/websocket/init/SpringWebSocketClientEventListener.java
index 338ad8d9a7..ee621e56ab 100644
---
a/shenyu-client/shenyu-client-websocket/shenyu-client-spring-websocket/src/main/java/org/apache/shenyu/client/spring/websocket/init/SpringWebSocketClientEventListener.java
+++
b/shenyu-client/shenyu-client-websocket/shenyu-client-spring-websocket/src/main/java/org/apache/shenyu/client/spring/websocket/init/SpringWebSocketClientEventListener.java
@@ -21,7 +21,6 @@ import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import
org.apache.shenyu.client.core.client.AbstractContextRefreshedEventListener;
import org.apache.shenyu.client.core.constant.ShenyuClientConstants;
-import
org.apache.shenyu.client.core.disruptor.ShenyuClientRegisterEventPublisher;
import org.apache.shenyu.client.core.utils.PortUtils;
import
org.apache.shenyu.client.spring.websocket.annotation.ShenyuServerEndpoint;
import
org.apache.shenyu.client.spring.websocket.annotation.ShenyuSpringWebSocketClient;
@@ -62,8 +61,6 @@ import java.util.Properties;
*/
public class SpringWebSocketClientEventListener extends
AbstractContextRefreshedEventListener<Object, ShenyuSpringWebSocketClient> {
- private final ShenyuClientRegisterEventPublisher publisher =
ShenyuClientRegisterEventPublisher.getInstance();
-
private final String[] pathAttributeNames = new String[] {"path", "value"};
private final List<Class<? extends Annotation>> mappingAnnotation = new
ArrayList<>(7);
@@ -105,9 +102,12 @@ public class SpringWebSocketClientEventListener extends
AbstractContextRefreshed
protected Map<String, Object> getBeans(final ApplicationContext context) {
// Filter out is not controller out
if (Boolean.TRUE.equals(isFull)) {
+ if (!markRegistered()) {
+ return Collections.emptyMap();
+ }
LOG.info("init spring websocket client success with isFull mode");
List<String> namespaceIds = super.getNamespace();
- namespaceIds.forEach(namespaceId ->
publisher.publishEvent(buildURIRegisterDTO(context, Collections.emptyMap(),
namespaceId)));
+ namespaceIds.forEach(namespaceId ->
getPublisher().publishEvent(buildURIRegisterDTO(context,
Collections.emptyMap(), namespaceId)));
return Collections.emptyMap();
}
Map<String, Object> endpointBeans =
context.getBeansWithAnnotation(ShenyuServerEndpoint.class);
diff --git
a/shenyu-client/shenyu-client-websocket/shenyu-client-spring-websocket/src/test/java/org/apache/shenyu/client/spring/websocket/init/SpringWebSocketClientEventListenerTest.java
b/shenyu-client/shenyu-client-websocket/shenyu-client-spring-websocket/src/test/java/org/apache/shenyu/client/spring/websocket/init/SpringWebSocketClientEventListenerTest.java
index 52058efd23..1c5de5420e 100644
---
a/shenyu-client/shenyu-client-websocket/shenyu-client-spring-websocket/src/test/java/org/apache/shenyu/client/spring/websocket/init/SpringWebSocketClientEventListenerTest.java
+++
b/shenyu-client/shenyu-client-websocket/shenyu-client-spring-websocket/src/test/java/org/apache/shenyu/client/spring/websocket/init/SpringWebSocketClientEventListenerTest.java
@@ -32,9 +32,10 @@ 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.MockitoAnnotations;
+import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationContext;
+import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.util.ReflectionUtils;
import java.lang.annotation.Annotation;
@@ -49,7 +50,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -84,21 +87,7 @@ public class SpringWebSocketClientEventListenerTest {
@BeforeEach
public void setUp() {
- MockitoAnnotations.openMocks(this);
- Properties properties = mock(Properties.class);
- when(properties.getProperty("appName")).thenReturn("appName");
- when(properties.getProperty("contextPath")).thenReturn("contextPath");
-
when(properties.getProperty(ShenyuClientConstants.PORT)).thenReturn("8080");
-
when(properties.getProperty(ShenyuClientConstants.HOST)).thenReturn("127.0.0.1");
-
when(properties.getProperty(ShenyuClientConstants.IP_PORT)).thenReturn("127.0.0.1:8080");
-
- ShenyuClientConfig clientConfig = mock(ShenyuClientConfig.class);
- Map<String, ClientPropertiesConfig> client = new HashMap<>();
- ClientPropertiesConfig clientPropertiesConfig = new
ClientPropertiesConfig();
- clientPropertiesConfig.setProps(properties);
- client.put(RpcTypeEnum.WEB_SOCKET.getName(), clientPropertiesConfig);
- when(clientConfig.getClient()).thenReturn(client);
- eventListener = new SpringWebSocketClientEventListener(clientConfig,
registerRepository);
+ eventListener = buildEventListener(false);
}
@Test
@@ -178,6 +167,39 @@ public class SpringWebSocketClientEventListenerTest {
assertEquals(port, "8080");
}
+ @Test
+ public void testOnApplicationEventFullModeShouldRegisterOnce() {
+ try (MockedStatic<ShenyuClientRegisterEventPublisher>
publisherMockedStatic = mockStatic(ShenyuClientRegisterEventPublisher.class)) {
+ ShenyuClientRegisterEventPublisher mockPublisher =
mock(ShenyuClientRegisterEventPublisher.class);
+
publisherMockedStatic.when(ShenyuClientRegisterEventPublisher::getInstance).thenReturn(mockPublisher);
+ SpringWebSocketClientEventListener fullModeEventListener =
buildEventListener(true);
+ ContextRefreshedEvent event = new
ContextRefreshedEvent(applicationContext);
+ fullModeEventListener.onApplicationEvent(event);
+ fullModeEventListener.onApplicationEvent(event);
+ verify(mockPublisher, times(1)).start(any());
+ verify(mockPublisher, times(1)).publishEvent(any());
+ }
+ }
+
+ private SpringWebSocketClientEventListener buildEventListener(final
boolean full) {
+ Properties properties = new Properties();
+ properties.setProperty("appName", "appName");
+ properties.setProperty("contextPath", "contextPath");
+ properties.setProperty(ShenyuClientConstants.PORT, "8080");
+ properties.setProperty(ShenyuClientConstants.HOST, "127.0.0.1");
+ properties.setProperty(ShenyuClientConstants.IP_PORT,
"127.0.0.1:8080");
+ properties.setProperty(ShenyuClientConstants.IS_FULL,
String.valueOf(full));
+ properties.setProperty(ShenyuClientConstants.DISCOVERY_LOCAL_MODE_KEY,
Boolean.TRUE.toString());
+
+ ShenyuClientConfig clientConfig = mock(ShenyuClientConfig.class);
+ Map<String, ClientPropertiesConfig> client = new HashMap<>();
+ ClientPropertiesConfig clientPropertiesConfig = new
ClientPropertiesConfig();
+ clientPropertiesConfig.setProps(properties);
+ client.put(RpcTypeEnum.WEB_SOCKET.getName(), clientPropertiesConfig);
+ when(clientConfig.getClient()).thenReturn(client);
+ return new SpringWebSocketClientEventListener(clientConfig,
registerRepository);
+ }
+
/**
* class for mock.
*/