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 e09eb35b68 fix(register): report the bound server port for gateway
instance identity (#7732) (#7861)
e09eb35b68 is described below
commit e09eb35b6837a7c54d6982f204ec3df7cecf2e96
Author: markxu <[email protected]>
AuthorDate: Tue Oct 20 12:36:46 2026 +0800
fix(register): report the bound server port for gateway instance identity
(#7732) (#7861)
Heartbeat, websocket sync, and HTTP long polling wait for the application
server's bound port and ignore the management listener. server.port=0 is
reported as the ephemeral port that was actually bound, and a missing or
non-positive client port is not published.
Fixes #7732
Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: markxu <[email protected]>
---
.../http/HttpLongPollingDataChangedListener.java | 4 +-
.../HttpLongPollingDataChangedListenerTest.java | 21 +++
.../shenyu/common/utils/BoundServerPort.java | 49 +++++++
.../shenyu/common/utils/BoundServerPortTest.java | 44 ++++++
.../register/client/beat/HeartbeatListener.java | 68 ++++++++--
.../client/beat/HeartbeatBoundPortTest.java | 130 ++++++++++++++++++
.../client/beat/HeartbeatListenerTest.java | 124 ++++++++++++++---
.../beat/HeartbeatListenerConfiguration.java | 7 +-
.../websocket/WebsocketSyncDataConfiguration.java | 8 +-
.../shenyu-sync-data-http/pom.xml | 5 +
.../shenyu/sync/data/http/HttpSyncDataService.java | 46 ++++++-
.../sync/data/http/HttpSyncBoundPortTest.java | 148 +++++++++++++++++++++
.../data/websocket/WebsocketSyncDataService.java | 81 +++++++----
.../data/websocket/WebsocketBoundPortTest.java | 117 ++++++++++++++++
.../websocket/WebsocketMasterRedirectTest.java | 13 +-
.../websocket/WebsocketSyncDataServiceTest.java | 13 +-
16 files changed, 786 insertions(+), 92 deletions(-)
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListener.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListener.java
index 9379a9b39c..f9fcd5976a 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListener.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListener.java
@@ -89,8 +89,6 @@ public class HttpLongPollingDataChangedListener extends
AbstractDataChangedListe
private static final String X_REAL_PORT = "X-Real-PORT";
- private static final String CLIENT_PORT_ZERO = "0";
-
/**
* Blocked client.
*/
@@ -146,7 +144,7 @@ public class HttpLongPollingDataChangedListener extends
AbstractDataChangedListe
// The header already contains JSON text; encoding it again adds a
string wrapper.
final String bootstrapInfo =
StringUtils.defaultString(request.getHeader(InstanceTypeConstants.BOOTSTRAP_INSTANCE_INFO),
"");
final String clientPort =
StringUtils.defaultString(request.getHeader(X_REAL_PORT), StringUtils.EMPTY);
- if (!CLIENT_PORT_ZERO.equals(clientPort)) {
+ if (NumberUtils.toInt(clientPort, -1) > 0) {
InstanceInfoReportEvent instanceInfoReportEvent =
InstanceInfoReportEvent.builder()
.instanceIp(clientIp)
.instancePort(clientPort)
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListenerTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListenerTest.java
index b926e8beb1..6aed2a0d53 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListenerTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListenerTest.java
@@ -438,6 +438,27 @@ public final class HttpLongPollingDataChangedListenerTest {
clearAllGroupsInCache(DEFAULT_NAMESPACE);
}
+ @Test
+ public void testDoLongPollingIgnoresPlaceholderClientPort() throws
Exception {
+ populateAllGroupsInCache(DEFAULT_NAMESPACE, "serverMd5");
+ try {
+ setAllGroupParamsDefault(this.httpServletRequest);
+
this.httpServletRequest.setParameter(ConfigGroupEnum.PLUGIN.name(),
"clientMd5,500");
+ this.httpServletRequest.addHeader("X-Real-PORT", "0");
+ listener.doLongPolling(this.httpServletRequest,
this.httpServletResponse);
+ Mockito.verify(instanceInfoReportEventPublisher,
Mockito.never()).publish(Mockito.any());
+
+ this.httpServletRequest = new MockHttpServletRequest();
+ this.httpServletResponse = new MockHttpServletResponse();
+ setAllGroupParamsDefault(this.httpServletRequest);
+
this.httpServletRequest.setParameter(ConfigGroupEnum.PLUGIN.name(),
"clientMd5,500");
+ listener.doLongPolling(this.httpServletRequest,
this.httpServletResponse);
+ Mockito.verify(instanceInfoReportEventPublisher,
Mockito.never()).publish(Mockito.any());
+ } finally {
+ clearAllGroupsInCache(DEFAULT_NAMESPACE);
+ }
+ }
+
/**
* test doLongPolling without optional instance information.
*/
diff --git
a/shenyu-common/src/main/java/org/apache/shenyu/common/utils/BoundServerPort.java
b/shenyu-common/src/main/java/org/apache/shenyu/common/utils/BoundServerPort.java
new file mode 100644
index 0000000000..11c2279fcd
--- /dev/null
+++
b/shenyu-common/src/main/java/org/apache/shenyu/common/utils/BoundServerPort.java
@@ -0,0 +1,49 @@
+/*
+ * 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.common.utils;
+
+import java.util.OptionalInt;
+
+/**
+ * Selects the port of the application server that should identify a gateway
instance.
+ */
+public final class BoundServerPort {
+
+ /**
+ * Namespace of Spring Boot's separate management server.
+ */
+ public static final String MANAGEMENT_NAMESPACE = "management";
+
+ private BoundServerPort() {
+ }
+
+ /**
+ * Accept a listener port only after the application server has bound it.
+ * A management server and any non-positive observation are not the
gateway listener.
+ *
+ * @param serverNamespace server namespace, empty for the primary
application server
+ * @param port observed port
+ * @return the bound application port, or empty when the observation must
not be reported
+ */
+ public static OptionalInt resolve(final String serverNamespace, final int
port) {
+ if (MANAGEMENT_NAMESPACE.equals(serverNamespace) || port <= 0) {
+ return OptionalInt.empty();
+ }
+ return OptionalInt.of(port);
+ }
+}
diff --git
a/shenyu-common/src/test/java/org/apache/shenyu/common/utils/BoundServerPortTest.java
b/shenyu-common/src/test/java/org/apache/shenyu/common/utils/BoundServerPortTest.java
new file mode 100644
index 0000000000..fb5b32f9eb
--- /dev/null
+++
b/shenyu-common/src/test/java/org/apache/shenyu/common/utils/BoundServerPortTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.common.utils;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.OptionalInt;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Test for {@link BoundServerPort}.
+ */
+class BoundServerPortTest {
+
+ @Test
+ void acceptsThePrimaryServerOnceItHasBound() {
+ assertEquals(OptionalInt.of(9195), BoundServerPort.resolve(null,
9195));
+ assertEquals(OptionalInt.of(18080), BoundServerPort.resolve("server",
18080));
+ }
+
+ @Test
+ void rejectsTheManagementServerAndUnboundPorts() {
+
assertTrue(BoundServerPort.resolve(BoundServerPort.MANAGEMENT_NAMESPACE,
19090).isEmpty());
+ assertTrue(BoundServerPort.resolve(null, 0).isEmpty());
+ assertTrue(BoundServerPort.resolve(null, -1).isEmpty());
+ }
+}
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 c393cda0b0..2fd6fd48bf 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
@@ -28,6 +28,7 @@ import org.apache.shenyu.common.config.ShenyuConfig;
import org.apache.shenyu.common.constant.Constants;
import org.apache.shenyu.common.constant.InstanceTypeConstants;
import org.apache.shenyu.common.utils.AesUtils;
+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;
@@ -37,12 +38,14 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.boot.web.context.WebServerInitializedEvent;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.EventListener;
import java.util.List;
+import java.util.Objects;
import java.util.Optional;
+import java.util.OptionalInt;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -71,10 +74,16 @@ public class HeartbeatListener {
private ShenyuBootstrapHeartBeatConfig config;
- public HeartbeatListener(final ShenyuBootstrapHeartBeatConfig config,
final ShenyuConfig shenyuConfig, final ServerProperties serverProperties) {
+ /**
+ * Port of the application listener. Zero until {@link
WebServerInitializedEvent}.
+ */
+ private volatile int boundPort;
+
+ private volatile boolean closed;
+
+ public HeartbeatListener(final ShenyuBootstrapHeartBeatConfig config,
final ShenyuConfig shenyuConfig) {
executor = new ScheduledThreadPoolExecutor(1,
ShenyuThreadFactory.create("scheduled-instance-task", false));
this.shenyuConfig = shenyuConfig;
- LOG.info("Web server initialized on port {}, starting heartbeat
reporter", serverProperties.getPort());
this.username = config.getProps().getProperty(Constants.USER_NAME);
this.password = config.getProps().getProperty(Constants.PASS_WORD);
this.config = config;
@@ -99,16 +108,48 @@ public class HeartbeatListener {
}
}
});
- executor.scheduleAtFixedRate(() -> {
- InstanceBeatInfoDTO instanceBeatInfoDTO = new
InstanceBeatInfoDTO();
-
instanceBeatInfoDTO.setInstancePort(String.valueOf(serverProperties.getPort()));
- instanceBeatInfoDTO.setInstanceIp(IpUtils.getHost());
- instanceBeatInfoDTO.setNamespaceId(shenyuConfig.getNamespace());
-
instanceBeatInfoDTO.setInstanceInfo(SystemInfoUtils.getSystemInfo());
-
instanceBeatInfoDTO.setInstanceType(InstanceTypeConstants.BOOTSTRAP_INSTANCE_TYPE);
- sendHeartbeat(instanceBeatInfoDTO);
- }, INITIAL_DELAY, CHECK_PERIOD, TimeUnit.SECONDS
- );
+ executor.scheduleAtFixedRate(this::reportHeartbeat, INITIAL_DELAY,
CHECK_PERIOD, TimeUnit.SECONDS);
+ }
+
+ /**
+ * 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.
+ *
+ * @param event web server initialization event
+ */
+ @EventListener
+ public void onWebServerInitialized(final WebServerInitializedEvent event) {
+ if (closed || Objects.isNull(event.getWebServer()) ||
Objects.isNull(event.getApplicationContext())) {
+ return;
+ }
+ OptionalInt resolved =
BoundServerPort.resolve(event.getApplicationContext().getServerNamespace(),
event.getWebServer().getPort());
+ if (resolved.isEmpty()) {
+ LOG.debug("Ignore heartbeat port from namespace {} port {}",
event.getApplicationContext().getServerNamespace(),
event.getWebServer().getPort());
+ return;
+ }
+ int observed = resolved.getAsInt();
+ if (boundPort == observed) {
+ return;
+ }
+ boundPort = observed;
+ LOG.info("Application server bound on port {}, heartbeat reporter will
use it", observed);
+ }
+
+ private void reportHeartbeat() {
+ if (closed) {
+ return;
+ }
+ int port = boundPort;
+ if (port <= 0) {
+ return;
+ }
+ InstanceBeatInfoDTO instanceBeatInfoDTO = new InstanceBeatInfoDTO();
+ instanceBeatInfoDTO.setInstancePort(String.valueOf(port));
+ instanceBeatInfoDTO.setInstanceIp(IpUtils.getHost());
+ instanceBeatInfoDTO.setNamespaceId(shenyuConfig.getNamespace());
+ instanceBeatInfoDTO.setInstanceInfo(SystemInfoUtils.getSystemInfo());
+
instanceBeatInfoDTO.setInstanceType(InstanceTypeConstants.BOOTSTRAP_INSTANCE_TYPE);
+ sendHeartbeat(instanceBeatInfoDTO);
}
private void sendHeartbeat(final InstanceBeatInfoDTO instanceBeatInfoDTO) {
@@ -129,6 +170,7 @@ public class HeartbeatListener {
@EventListener(ContextClosedEvent.class)
public void onShutdown() {
+ closed = true;
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
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
new file mode 100644
index 0000000000..642ee875e0
--- /dev/null
+++
b/shenyu-register-center/shenyu-register-client-beat/src/test/java/org/apache/shenyu/register/client/beat/HeartbeatBoundPortTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.register.client.beat;
+
+import org.apache.shenyu.common.config.ShenyuConfig;
+import org.apache.shenyu.common.constant.Constants;
+import org.apache.shenyu.common.utils.BoundServerPort;
+import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.common.utils.SystemInfoUtils;
+import org.apache.shenyu.register.common.dto.InstanceBeatInfoDTO;
+import org.apache.shenyu.register.common.utils.RegisterUtils;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedConstruction;
+import org.mockito.MockedStatic;
+import
org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
+import
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
+import
org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent;
+import org.springframework.boot.web.server.WebServer;
+
+import java.lang.reflect.Method;
+import java.net.ServerSocket;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+/**
+ * Binds real listeners the way {@code server.port=0} and a fixed port do,
then checks the reported identity.
+ */
+class HeartbeatBoundPortTest {
+
+ @Test
+ void reportsDistinctBoundPortsForEphemeralAndFixedListeners() throws
Exception {
+ final WebServer first = start(0);
+ final WebServer second = start(0);
+ int fixedPort;
+ try (ServerSocket reserved = new ServerSocket(0)) {
+ fixedPort = reserved.getLocalPort();
+ }
+ final WebServer fixed = start(fixedPort);
+ final WebServer management = start(0);
+ try (MockedConstruction<ScheduledThreadPoolExecutor> ignoredExecutors
= mockConstruction(ScheduledThreadPoolExecutor.class);
+ MockedStatic<SystemInfoUtils> ignored =
mockStatic(SystemInfoUtils.class);
+ MockedStatic<RegisterUtils> register =
mockStatic(RegisterUtils.class)) {
+ register.when(() -> RegisterUtils.doLogin(anyString(),
anyString(), anyString())).thenReturn(Optional.of("mock-token"));
+ final HeartbeatListener ephemeral = listener();
+ final HeartbeatListener other = listener();
+ final HeartbeatListener explicit = listener();
+ publish(ephemeral, first, null);
+ publish(ephemeral, management,
BoundServerPort.MANAGEMENT_NAMESPACE);
+ publish(other, second, null);
+ publish(explicit, fixed, null);
+ final InstanceBeatInfoDTO firstBeat = report(ephemeral, register);
+ final InstanceBeatInfoDTO secondBeat = report(other, register);
+ final InstanceBeatInfoDTO fixedBeat = report(explicit, register);
+ assertTrue(first.getPort() > 0);
+ assertEquals(String.valueOf(first.getPort()),
firstBeat.getInstancePort());
+ assertEquals(String.valueOf(second.getPort()),
secondBeat.getInstancePort());
+ assertEquals(String.valueOf(fixedPort),
fixedBeat.getInstancePort());
+ assertNotEquals(firstBeat.getInstancePort(),
secondBeat.getInstancePort());
+ assertNotEquals(management.getPort(),
Integer.parseInt(firstBeat.getInstancePort()));
+ ephemeral.onShutdown();
+ other.onShutdown();
+ explicit.onShutdown();
+ } finally {
+ first.stop();
+ second.stop();
+ fixed.stop();
+ management.stop();
+ }
+ }
+
+ private InstanceBeatInfoDTO report(final HeartbeatListener listener, final
MockedStatic<RegisterUtils> register) throws Exception {
+ Method reportHeartbeat =
HeartbeatListener.class.getDeclaredMethod("reportHeartbeat");
+ reportHeartbeat.setAccessible(true);
+ reportHeartbeat.invoke(listener);
+ ArgumentCaptor<String> payload = ArgumentCaptor.forClass(String.class);
+ register.verify(() -> RegisterUtils.doHeartBeat(payload.capture(),
anyString(), anyString(), anyString()));
+ register.clearInvocations();
+ return GsonUtils.getInstance().fromJson(payload.getValue(),
InstanceBeatInfoDTO.class);
+ }
+
+ private void publish(final HeartbeatListener listener, final WebServer
server, final String namespace) {
+ ServletWebServerApplicationContext applicationContext =
mock(ServletWebServerApplicationContext.class);
+ when(applicationContext.getServerNamespace()).thenReturn(namespace);
+ listener.onWebServerInitialized(new
ServletWebServerInitializedEvent(server, applicationContext));
+ }
+
+ private HeartbeatListener listener() {
+ Properties props = new Properties();
+ props.setProperty(Constants.USER_NAME, "admin");
+ props.setProperty(Constants.PASS_WORD, "123456");
+ ShenyuBootstrapHeartBeatConfig config = new
ShenyuBootstrapHeartBeatConfig();
+ config.setServerLists("http://127.0.0.1:1");
+ config.setProps(props);
+ ShenyuConfig shenyuConfig = new ShenyuConfig();
+ shenyuConfig.setNamespace("shenyu");
+ return new HeartbeatListener(config, shenyuConfig);
+ }
+
+ private WebServer start(final int port) {
+ WebServer server = new
TomcatServletWebServerFactory(port).getWebServer();
+ server.start();
+ return server;
+ }
+}
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 311127594f..2c9c33a374 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
@@ -32,7 +32,9 @@ import org.mockito.MockedConstruction;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
-import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.boot.web.context.WebServerApplicationContext;
+import org.springframework.boot.web.context.WebServerInitializedEvent;
+import org.springframework.boot.web.server.WebServer;
import java.lang.reflect.Field;
import java.util.Optional;
@@ -43,9 +45,11 @@ import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
+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;
@ExtendWith(MockitoExtension.class)
class HeartbeatListenerTest {
@@ -56,13 +60,10 @@ class HeartbeatListenerTest {
private ShenyuConfig shenyuConfig;
- private ServerProperties serverProperties;
-
@BeforeEach
void setUp() {
config = createMockConfig();
shenyuConfig = createMockShenyuConfig();
- serverProperties = createMockServerProperties();
}
private ShenyuBootstrapHeartBeatConfig createMockConfig() {
@@ -86,14 +87,6 @@ class HeartbeatListenerTest {
return config;
}
- private ServerProperties createMockServerProperties() {
-
- ServerProperties properties = new ServerProperties();
- properties.setPort(8080);
-
- return properties;
- }
-
@Test
void testPeriodicHeartbeatSurvivesFailuresOnEveryServer() throws
IOException {
try (MockedConstruction<ScheduledThreadPoolExecutor> executors =
Mockito.mockConstruction(ScheduledThreadPoolExecutor.class);
@@ -102,7 +95,8 @@ class HeartbeatListenerTest {
register.when(() -> RegisterUtils.doLogin(anyString(),
anyString(), anyString())).thenReturn(Optional.of("mock-token"));
register.when(() -> RegisterUtils.doHeartBeat(anyString(),
anyString(), anyString(), anyString()))
.thenThrow(new IOException("admin unavailable"));
- heartbeatListener = new HeartbeatListener(config, shenyuConfig,
serverProperties);
+ heartbeatListener = new HeartbeatListener(config, shenyuConfig);
+ bind(heartbeatListener, null, 8080);
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));
assertDoesNotThrow(periodic.getValue()::run);
@@ -120,7 +114,8 @@ class HeartbeatListenerTest {
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,
serverProperties);
+ heartbeatListener = new HeartbeatListener(config, shenyuConfig);
+ bind(heartbeatListener, null, 8080);
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();
@@ -128,7 +123,92 @@ class HeartbeatListenerTest {
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
@@ -139,7 +219,7 @@ class HeartbeatListenerTest {
.thenReturn(Optional.of("mock-token"));
assertDoesNotThrow(() -> {
- heartbeatListener = new HeartbeatListener(config,
shenyuConfig, serverProperties);
+ heartbeatListener = new HeartbeatListener(config,
shenyuConfig);
});
assertNotNull(heartbeatListener);
@@ -164,7 +244,7 @@ class HeartbeatListenerTest {
.thenReturn(Optional.of("mock-token"));
assertDoesNotThrow(() -> {
- heartbeatListener = new
HeartbeatListener(configWithEncryption, shenyuConfig, serverProperties);
+ heartbeatListener = new
HeartbeatListener(configWithEncryption, shenyuConfig);
});
assertNotNull(heartbeatListener);
@@ -181,7 +261,7 @@ class HeartbeatListenerTest {
registerUtilsMockedStatic.when(() ->
RegisterUtils.doHeartBeat(anyString(), anyString(), anyString(), anyString()))
.then(invocation -> null);
- heartbeatListener = new HeartbeatListener(config, shenyuConfig,
serverProperties);
+ heartbeatListener = new HeartbeatListener(config, shenyuConfig);
// Use reflection to access private method sendHeartbeat
java.lang.reflect.Method sendHeartbeatMethod =
HeartbeatListener.class.getDeclaredMethod("sendHeartbeat",
@@ -215,7 +295,7 @@ class HeartbeatListenerTest {
registerUtilsMockedStatic.when(() ->
RegisterUtils.doLogin(anyString(), anyString(), anyString()))
.thenReturn(Optional.empty());
- heartbeatListener = new HeartbeatListener(config, shenyuConfig,
serverProperties);
+ heartbeatListener = new HeartbeatListener(config, shenyuConfig);
// Use reflection to access private method sendHeartbeat
java.lang.reflect.Method sendHeartbeatMethod =
HeartbeatListener.class.getDeclaredMethod("sendHeartbeat",
@@ -239,7 +319,7 @@ class HeartbeatListenerTest {
registerUtilsMockedStatic.when(() ->
RegisterUtils.doLogin(anyString(), anyString(), anyString()))
.thenReturn(Optional.of("mock-token"));
- heartbeatListener = new HeartbeatListener(config, shenyuConfig,
serverProperties);
+ heartbeatListener = new HeartbeatListener(config, shenyuConfig);
// Use reflection to access the executor
Field executorField =
HeartbeatListener.class.getDeclaredField("executor");
@@ -262,7 +342,7 @@ class HeartbeatListenerTest {
registerUtilsMockedStatic.when(() ->
RegisterUtils.doLogin(anyString(), anyString(), anyString()))
.thenReturn(Optional.of("mock-token"));
- heartbeatListener = new HeartbeatListener(config, shenyuConfig,
serverProperties);
+ heartbeatListener = new HeartbeatListener(config, shenyuConfig);
// Use reflection to check private fields
Field usernameField =
HeartbeatListener.class.getDeclaredField("username");
@@ -301,7 +381,7 @@ class HeartbeatListenerTest {
Mockito.contains("localhost:9096"), anyString(),
anyString()))
.then(invocation -> null);
- heartbeatListener = new HeartbeatListener(config, shenyuConfig,
serverProperties);
+ heartbeatListener = new HeartbeatListener(config, shenyuConfig);
// Use reflection to access private method sendHeartbeat
java.lang.reflect.Method sendHeartbeatMethod =
HeartbeatListener.class.getDeclaredMethod("sendHeartbeat",
@@ -332,7 +412,7 @@ class HeartbeatListenerTest {
registerUtilsMockedStatic.when(() ->
RegisterUtils.doHeartBeat(anyString(), anyString(), anyString(), anyString()))
.thenThrow(new RuntimeException("Connection failed"));
- heartbeatListener = new HeartbeatListener(config, shenyuConfig,
serverProperties);
+ heartbeatListener = new HeartbeatListener(config, shenyuConfig);
// Use reflection to access private method sendHeartbeat
java.lang.reflect.Method sendHeartbeatMethod =
HeartbeatListener.class.getDeclaredMethod("sendHeartbeat",
diff --git
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/src/main/java/org/apache/shenyu/register/client/beat/HeartbeatListenerConfiguration.java
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/src/main/java/org/apache/shenyu/register/client/beat/HeartbeatListenerConfiguration.java
index 2fd1a464f3..dfbbe4eaaf 100644
---
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/src/main/java/org/apache/shenyu/register/client/beat/HeartbeatListenerConfiguration.java
+++
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-client/shenyu-spring-boot-starter-client-beat/src/main/java/org/apache/shenyu/register/client/beat/HeartbeatListenerConfiguration.java
@@ -19,7 +19,6 @@ package org.apache.shenyu.register.client.beat;
import org.apache.shenyu.common.config.ShenyuConfig;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
-import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -37,14 +36,12 @@ public class HeartbeatListenerConfiguration {
*
* @param shenyuBootstrapHeartBeatConfig the shenyuBootstrapHeartBeatConfig
* @param shenyuConfig the shenyu config
- * @param serverProperties the server properties
* @return the heartbeat bean listener.
*/
@Bean
public HeartbeatListener heartbeatListener(final
ShenyuBootstrapHeartBeatConfig shenyuBootstrapHeartBeatConfig,
- final ShenyuConfig shenyuConfig,
- final ServerProperties
serverProperties) {
- return new HeartbeatListener(shenyuBootstrapHeartBeatConfig,
shenyuConfig, serverProperties);
+ final ShenyuConfig
shenyuConfig) {
+ return new HeartbeatListener(shenyuBootstrapHeartBeatConfig,
shenyuConfig);
}
/**
diff --git
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sync-data-center/shenyu-spring-boot-starter-sync-data-websocket/src/main/java/org/apache/shenyu/springboot/starter/sync/data/websocket/WebsocketSyncDataConfiguration.java
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sync-data-center/shenyu-spring-boot-starter-sync-data-websocket/src/main/java/org/apache/shenyu/springboot/starter/sync/data/websocket/WebsocketSyncDataConfiguration.java
index f6047ff711..d2bfe143e3 100644
---
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sync-data-center/shenyu-spring-boot-starter-sync-data-websocket/src/main/java/org/apache/shenyu/springboot/starter/sync/data/websocket/WebsocketSyncDataConfiguration.java
+++
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sync-data-center/shenyu-spring-boot-starter-sync-data-websocket/src/main/java/org/apache/shenyu/springboot/starter/sync/data/websocket/WebsocketSyncDataConfiguration.java
@@ -31,7 +31,6 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -62,7 +61,6 @@ public class WebsocketSyncDataConfiguration {
* @param proxySelectorSubscribers the proxySelector subscribers
* @param discoveryUpstreamSubscribers the discoveryUpstream subscribers
* @param aiProxyApiKeySubscribers the ai proxy api key subscribers
- * @param serverProperties the serverProperties
* @return the sync data service
*/
@Bean
@@ -75,8 +73,7 @@ public class WebsocketSyncDataConfiguration {
final ObjectProvider<List<ProxySelectorDataSubscriber>>
proxySelectorSubscribers,
final ObjectProvider<List<DiscoveryUpstreamDataSubscriber>>
discoveryUpstreamSubscribers,
- final ObjectProvider<List<AiProxyApiKeyDataSubscriber>>
aiProxyApiKeySubscribers,
- final ServerProperties serverProperties) {
+ final ObjectProvider<List<AiProxyApiKeyDataSubscriber>>
aiProxyApiKeySubscribers) {
LOGGER.info("you use websocket sync shenyu data.......");
return new WebsocketSyncDataService(
websocketConfig.getIfAvailable(WebsocketConfig::new),
@@ -86,8 +83,7 @@ public class WebsocketSyncDataConfiguration {
authSubscribers.getIfAvailable(Collections::emptyList),
proxySelectorSubscribers.getIfAvailable(Collections::emptyList),
discoveryUpstreamSubscribers.getIfAvailable(Collections::emptyList),
-
aiProxyApiKeySubscribers.getIfAvailable(Collections::emptyList),
- serverProperties);
+
aiProxyApiKeySubscribers.getIfAvailable(Collections::emptyList));
}
/**
diff --git a/shenyu-sync-data-center/shenyu-sync-data-http/pom.xml
b/shenyu-sync-data-center/shenyu-sync-data-http/pom.xml
index 3639fda2cf..7525caf9a5 100644
--- a/shenyu-sync-data-center/shenyu-sync-data-http/pom.xml
+++ b/shenyu-sync-data-center/shenyu-sync-data-http/pom.xml
@@ -60,5 +60,10 @@
<artifactId>spring-web</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot</artifactId>
+ <scope>provided</scope>
+ </dependency>
</dependencies>
</project>
diff --git
a/shenyu-sync-data-center/shenyu-sync-data-http/src/main/java/org/apache/shenyu/sync/data/http/HttpSyncDataService.java
b/shenyu-sync-data-center/shenyu-sync-data-http/src/main/java/org/apache/shenyu/sync/data/http/HttpSyncDataService.java
index 75ecd3520f..ab26186f65 100644
---
a/shenyu-sync-data-center/shenyu-sync-data-http/src/main/java/org/apache/shenyu/sync/data/http/HttpSyncDataService.java
+++
b/shenyu-sync-data-center/shenyu-sync-data-http/src/main/java/org/apache/shenyu/sync/data/http/HttpSyncDataService.java
@@ -36,6 +36,7 @@ import
org.apache.shenyu.common.constant.InstanceTypeConstants;
import org.apache.shenyu.common.dto.ConfigData;
import org.apache.shenyu.common.enums.ConfigGroupEnum;
import org.apache.shenyu.common.exception.ShenyuException;
+import org.apache.shenyu.common.utils.BoundServerPort;
import org.apache.shenyu.common.utils.GsonUtils;
import org.apache.shenyu.common.utils.SystemInfoUtils;
import org.apache.shenyu.common.utils.ThreadUtils;
@@ -50,7 +51,8 @@ import org.apache.shenyu.sync.data.http.config.HttpConfig;
import org.apache.shenyu.sync.data.http.refresh.DataRefreshFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.web.context.WebServerInitializedEvent;
+import org.springframework.context.event.EventListener;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -60,6 +62,7 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
+import java.util.OptionalInt;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
@@ -90,8 +93,10 @@ public class HttpSyncDataService implements SyncDataService {
private final ShenyuConfig shenyuConfig;
- @Value("${server.port}")
- private int port;
+ /**
+ * Port of the application listener. Zero until the server initialization
event.
+ */
+ private volatile int boundPort;
public HttpSyncDataService(final HttpConfig httpConfig,
final PluginDataSubscriber pluginDataSubscriber,
@@ -118,6 +123,30 @@ public class HttpSyncDataService implements
SyncDataService {
this.start();
}
+ /**
+ * Record the application listener port. The management server and any
port that is not yet bound
+ * are ignored, so long polling does not publish a placeholder.
+ *
+ * @param event web server initialization event
+ */
+ @EventListener
+ public void onWebServerInitialized(final WebServerInitializedEvent event) {
+ if (Objects.isNull(event.getWebServer()) ||
Objects.isNull(event.getApplicationContext())) {
+ return;
+ }
+ OptionalInt resolved =
BoundServerPort.resolve(event.getApplicationContext().getServerNamespace(),
event.getWebServer().getPort());
+ if (resolved.isEmpty()) {
+ LOG.debug("Ignore http sync port from namespace {} port {}",
event.getApplicationContext().getServerNamespace(),
event.getWebServer().getPort());
+ return;
+ }
+ int observed = resolved.getAsInt();
+ if (boundPort == observed) {
+ return;
+ }
+ boundPort = observed;
+ LOG.info("Application server bound on port {}, http sync will report
it", observed);
+ }
+
private void start() {
// It could be initialized multiple times, so you need to control that.
if (RUNNING.compareAndSet(false, true)) {
@@ -212,12 +241,15 @@ public class HttpSyncDataService implements
SyncDataService {
}
params.put("namespaceId",
Lists.newArrayList(shenyuConfig.getNamespace()));
LOG.debug("listener params: [{}]", params);
- Headers headers = new Headers.Builder()
+ Headers.Builder headerBuilder = new Headers.Builder()
.add(Constants.X_ACCESS_TOKEN,
this.accessTokenManager.getAccessToken())
.add("Content-Type", "application/x-www-form-urlencoded")
- .add("X-Real-PORT", port + "")
- .add(InstanceTypeConstants.BOOTSTRAP_INSTANCE_INFO,
SystemInfoUtils.getSystemInfo())
- .build();
+ .add(InstanceTypeConstants.BOOTSTRAP_INSTANCE_INFO,
SystemInfoUtils.getSystemInfo());
+ int port = boundPort;
+ if (port > 0) {
+ headerBuilder.add("X-Real-PORT", String.valueOf(port));
+ }
+ Headers headers = headerBuilder.build();
String listenerUrl = server +
Constants.SHENYU_ADMIN_PATH_CONFIGS_LISTENER;
String uri =
UriComponentsBuilder.fromHttpUrl(listenerUrl).queryParams(params).build(true).toUriString();
Request request = new Request.Builder()
diff --git
a/shenyu-sync-data-center/shenyu-sync-data-http/src/test/java/org/apache/shenyu/sync/data/http/HttpSyncBoundPortTest.java
b/shenyu-sync-data-center/shenyu-sync-data-http/src/test/java/org/apache/shenyu/sync/data/http/HttpSyncBoundPortTest.java
new file mode 100644
index 0000000000..791560bbc1
--- /dev/null
+++
b/shenyu-sync-data-center/shenyu-sync-data-http/src/test/java/org/apache/shenyu/sync/data/http/HttpSyncBoundPortTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.sync.data.http;
+
+import okhttp3.Call;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import org.apache.shenyu.common.config.ShenyuConfig;
+import org.apache.shenyu.common.utils.BoundServerPort;
+import org.apache.shenyu.sync.data.api.PluginDataSubscriber;
+import org.apache.shenyu.sync.data.http.config.HttpConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.web.context.WebServerApplicationContext;
+import org.springframework.boot.web.context.WebServerInitializedEvent;
+import org.springframework.boot.web.server.WebServer;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.net.ServerSocket;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+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.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * HTTP long polling reports the bound application port and omits a
placeholder.
+ */
+class HttpSyncBoundPortTest {
+
+ @Test
+ void reportsDistinctBoundPortsAndSkipsPlaceholders() throws Exception {
+ try (ServerSocket firstSocket = new ServerSocket(0);
+ ServerSocket secondSocket = new ServerSocket(0);
+ ServerSocket fixedSocket = new ServerSocket(0)) {
+ final int firstPort = firstSocket.getLocalPort();
+ final int secondPort = secondSocket.getLocalPort();
+ final int fixedPort = fixedSocket.getLocalPort();
+ assertTrue(firstPort > 0);
+ assertNotEquals(firstPort, secondPort);
+ HttpSyncDataService first = service();
+ HttpSyncDataService second = service();
+ try {
+ assertNull(poll(first).header("X-Real-PORT"));
+ publish(first, 0, null);
+ publish(first, secondPort,
BoundServerPort.MANAGEMENT_NAMESPACE);
+ assertNull(poll(first).header("X-Real-PORT"));
+ publish(first, firstPort, null);
+ publish(second, secondPort, null);
+ publish(second, fixedPort, "server");
+ assertEquals(String.valueOf(firstPort),
poll(first).header("X-Real-PORT"));
+ assertEquals(String.valueOf(fixedPort),
poll(second).header("X-Real-PORT"));
+ assertNotEquals(poll(first).header("X-Real-PORT"),
poll(second).header("X-Real-PORT"));
+ } finally {
+ first.close();
+ second.close();
+ }
+ }
+ }
+
+ private Request poll(final HttpSyncDataService service) {
+ AtomicReference<Request> captured = new AtomicReference<>();
+ OkHttpClient client = (OkHttpClient)
ReflectionTestUtils.getField(service, "okHttpClient");
+
when(Objects.requireNonNull(client).newCall(any(Request.class))).thenAnswer(invocation
-> {
+ captured.set(invocation.getArgument(0));
+ return listenerCall();
+ });
+ ReflectionTestUtils.invokeMethod(service, "doLongPolling",
"http://127.0.0.1:1");
+ return Objects.requireNonNull(captured.get());
+ }
+
+ private void publish(final HttpSyncDataService service, final int port,
final String namespace) {
+ WebServer webServer = mock(WebServer.class);
+ when(webServer.getPort()).thenReturn(port);
+ WebServerApplicationContext applicationContext =
mock(WebServerApplicationContext.class);
+ when(applicationContext.getServerNamespace()).thenReturn(namespace);
+ WebServerInitializedEvent event =
mock(WebServerInitializedEvent.class);
+ when(event.getWebServer()).thenReturn(webServer);
+ when(event.getApplicationContext()).thenReturn(applicationContext);
+ service.onWebServerInitialized(event);
+ }
+
+ private HttpSyncDataService service() throws Exception {
+ OkHttpClient client = mock(OkHttpClient.class);
+ when(client.newCall(any(Request.class))).thenAnswer(invocation ->
fetchCall());
+ HttpConfig httpConfig = new HttpConfig();
+ httpConfig.setUrl("http://127.0.0.1:1");
+ ShenyuConfig shenyuConfig = new ShenyuConfig();
+ shenyuConfig.setNamespace("shenyu");
+ AccessTokenManager accessTokenManager = mock(AccessTokenManager.class);
+ when(accessTokenManager.getAccessToken()).thenReturn("token");
+ HttpSyncDataService service = new HttpSyncDataService(httpConfig,
mock(PluginDataSubscriber.class), client,
+ Collections.emptyList(), Collections.emptyList(),
Collections.emptyList(), Collections.emptyList(),
+ Collections.emptyList(), accessTokenManager, shenyuConfig);
+ ExecutorService executor = (ExecutorService)
ReflectionTestUtils.getField(service, "executor");
+ service.close();
+ if (Objects.nonNull(executor)) {
+ executor.awaitTermination(2, TimeUnit.SECONDS);
+ }
+ return service;
+ }
+
+ private Call fetchCall() throws Exception {
+ Call call = mock(Call.class);
+ Response response = mock(Response.class);
+ ResponseBody body = mock(ResponseBody.class);
+ when(response.isSuccessful()).thenReturn(true);
+ when(response.body()).thenReturn(body);
+ when(body.string()).thenReturn("{\"code\":200,\"data\":{}}");
+ when(call.execute()).thenReturn(response);
+ return call;
+ }
+
+ private Call listenerCall() throws Exception {
+ Call call = mock(Call.class);
+ Response response = mock(Response.class);
+ ResponseBody body = mock(ResponseBody.class);
+ when(response.isSuccessful()).thenReturn(true);
+ when(response.body()).thenReturn(body);
+ when(body.string()).thenReturn("{\"data\":null}");
+ when(call.execute()).thenReturn(response);
+ return call;
+ }
+}
diff --git
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketSyncDataService.java
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketSyncDataService.java
index 76f3dc8946..e006e7f804 100644
---
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketSyncDataService.java
+++
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketSyncDataService.java
@@ -23,6 +23,7 @@ import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shenyu.common.config.ShenyuConfig;
import org.apache.shenyu.common.constant.Constants;
+import org.apache.shenyu.common.utils.BoundServerPort;
import org.apache.shenyu.common.enums.RunningModeEnum;
import org.apache.shenyu.common.timer.AbstractRoundTask;
import org.apache.shenyu.common.timer.TaskEntity;
@@ -39,9 +40,11 @@ import
org.apache.shenyu.sync.data.api.ProxySelectorDataSubscriber;
import org.apache.shenyu.sync.data.api.SyncDataService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.boot.web.context.WebServerInitializedEvent;
+import org.springframework.context.event.EventListener;
import java.net.URI;
+import java.util.OptionalInt;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
@@ -93,7 +96,10 @@ public class WebsocketSyncDataService implements
SyncDataService {
private boolean closed;
- private final ServerProperties serverProperties;
+ /**
+ * Port of the application listener. Zero until the server initialization
event.
+ */
+ private volatile int boundPort;
/**
* Instantiates a new Websocket sync cache.
@@ -105,7 +111,7 @@ public class WebsocketSyncDataService implements
SyncDataService {
* @param authDataSubscribers the auth data subscribers
* @param proxySelectorDataSubscribers the proxy selector data subscribers
* @param discoveryUpstreamDataSubscribers the discovery upstream data
subscribers
- * @param serverProperties serverProperties
+ * @param aiProxyApiKeyDataSubscribers the ai proxy api key subscribers
*/
public WebsocketSyncDataService(
final WebsocketConfig websocketConfig,
@@ -116,8 +122,7 @@ public class WebsocketSyncDataService implements
SyncDataService {
final List<ProxySelectorDataSubscriber>
proxySelectorDataSubscribers,
final List<DiscoveryUpstreamDataSubscriber>
discoveryUpstreamDataSubscribers,
final
List<org.apache.shenyu.sync.data.api.AiProxyApiKeyDataSubscriber>
- aiProxyApiKeyDataSubscribers,
- final ServerProperties serverProperties) {
+ aiProxyApiKeyDataSubscribers) {
this.timer = WheelTimerFactory.newWheelTimer();
this.websocketConfig = websocketConfig;
this.pluginDataSubscriber = pluginDataSubscriber;
@@ -127,12 +132,6 @@ public class WebsocketSyncDataService implements
SyncDataService {
this.discoveryUpstreamDataSubscribers =
discoveryUpstreamDataSubscribers;
this.aiProxyApiKeyDataSubscribers = aiProxyApiKeyDataSubscribers;
this.namespaceId = shenyuConfig.getNamespace();
- this.serverProperties = serverProperties;
- LOG.info("start init connecting...");
- List<String> urls = websocketConfig.getUrls();
- for (String url : urls) {
- clients.add(createClient(url));
- }
LOG.info("start check task...");
long intervalSeconds = websocketConfig.getMasterCheckInterval() <= 0 ?
60 : websocketConfig.getMasterCheckInterval();
this.timer.add(timerTask = new AbstractRoundTask(null,
TimeUnit.SECONDS.toMillis(intervalSeconds)) {
@@ -151,12 +150,7 @@ public class WebsocketSyncDataService implements
SyncDataService {
LOG.debug("master checking task start...");
}
if (CollectionUtils.isEmpty(clients)) {
- List<String> urls = websocketConfig.getUrls();
- if (CollectionUtils.isNotEmpty(urls)) {
- for (String url : urls) {
- clients.add(createClient(url));
- }
- }
+ addSeedClients();
}
Set<URI> masterUrls = new LinkedHashSet<>();
Iterator<ShenyuWebsocketClient> iterator = clients.iterator();
@@ -188,7 +182,7 @@ public class WebsocketSyncDataService implements
SyncDataService {
}
}
for (URI masterUri : masterUrls) {
- if (clients.stream().noneMatch(client ->
masterUri.equals(client.getURI()))) {
+ if (boundPort > 0 && clients.stream().noneMatch(client ->
masterUri.equals(client.getURI()))) {
try {
clients.add(createClient(masterUri.toString()));
} catch (RuntimeException ex) {
@@ -197,12 +191,49 @@ public class WebsocketSyncDataService implements
SyncDataService {
}
}
if (CollectionUtils.isEmpty(clients)) {
- List<String> urls = websocketConfig.getUrls();
- if (CollectionUtils.isNotEmpty(urls)) {
- for (String url : urls) {
- clients.add(createClient(url));
- }
- }
+ addSeedClients();
+ }
+ }
+
+ /**
+ * Connect using the application listener port. A management server is not
the gateway listener,
+ * and clients stay disconnected until a positive bound port is known.
Closing the service first
+ * discards a later initialization event.
+ *
+ * @param event web server initialization event
+ */
+ @EventListener
+ public synchronized void onWebServerInitialized(final
WebServerInitializedEvent event) {
+ if (closed || Objects.isNull(event.getWebServer()) ||
Objects.isNull(event.getApplicationContext())) {
+ return;
+ }
+ OptionalInt resolved =
BoundServerPort.resolve(event.getApplicationContext().getServerNamespace(),
event.getWebServer().getPort());
+ if (resolved.isEmpty()) {
+ LOG.debug("Ignore websocket client port from namespace {} port
{}", event.getApplicationContext().getServerNamespace(),
event.getWebServer().getPort());
+ return;
+ }
+ int observed = resolved.getAsInt();
+ if (boundPort == observed) {
+ return;
+ }
+ boundPort = observed;
+ LOG.info("Application server bound on port {}, websocket sync will
report it", observed);
+ if (CollectionUtils.isEmpty(clients)) {
+ addSeedClients();
+ }
+ }
+
+ private void addSeedClients() {
+ if (boundPort <= 0) {
+ return;
+ }
+ List<String> urls = websocketConfig.getUrls();
+ if (CollectionUtils.isEmpty(urls)) {
+ return;
+ }
+ LOG.info("start init connecting on port {}...", boundPort);
+ for (String url : urls) {
+ clients.add(createClient(url));
}
}
@@ -291,7 +322,7 @@ public class WebsocketSyncDataService implements
SyncDataService {
discoveryUpstreamDataSubscribers,
this.aiProxyApiKeyDataSubscribers,
namespaceId,
- serverProperties.getPort(),
+ boundPort,
websocketConfig.isInitialSyncReadiness() ? initialSyncReady :
null,
this::onMasterLost);
}
diff --git
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketBoundPortTest.java
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketBoundPortTest.java
new file mode 100644
index 0000000000..cafb5e4315
--- /dev/null
+++
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketBoundPortTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.plugin.sync.data.websocket;
+
+import org.apache.shenyu.common.config.ShenyuConfig;
+import org.apache.shenyu.common.utils.BoundServerPort;
+import
org.apache.shenyu.plugin.sync.data.websocket.client.ShenyuWebsocketClient;
+import org.apache.shenyu.plugin.sync.data.websocket.config.WebsocketConfig;
+import org.apache.shenyu.sync.data.api.PluginDataSubscriber;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedConstruction;
+import
org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
+import org.springframework.boot.web.server.WebServer;
+import
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
+import
org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent;
+
+import java.lang.reflect.Field;
+import java.net.ServerSocket;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.when;
+
+/**
+ * Websocket clients report the port Tomcat actually bound, including {@code
server.port=0}.
+ */
+class WebsocketBoundPortTest {
+
+ @Test
+ void waitsForTheBoundPortAndKeepsDistinctIdentities() throws Exception {
+ final WebServer ephemeral = start(0);
+ final WebServer other = start(0);
+ int fixedPort;
+ try (ServerSocket reserved = new ServerSocket(0)) {
+ fixedPort = reserved.getLocalPort();
+ }
+ final WebServer fixed = start(fixedPort);
+ final WebServer management = start(0);
+ List<Integer> reportedPorts = new ArrayList<>();
+ try (MockedConstruction<ShenyuWebsocketClient> clients =
mockConstruction(ShenyuWebsocketClient.class,
+ (mock, context) -> reportedPorts.add((Integer)
context.arguments().get(9)))) {
+ final WebsocketSyncDataService first = service();
+ assertTrue(clientList(first).isEmpty());
+ publish(first, management, BoundServerPort.MANAGEMENT_NAMESPACE);
+ publish(first, ephemeral, null);
+ final WebsocketSyncDataService second = service();
+ publish(second, other, null);
+ WebsocketSyncDataService explicit = service();
+ publish(explicit, fixed, null);
+ assertEquals(1, clientList(first).size());
+ assertEquals(ephemeral.getPort(), reportedPorts.get(0));
+ assertEquals(other.getPort(), reportedPorts.get(1));
+ assertEquals(fixedPort, reportedPorts.get(2));
+ assertNotEquals(ephemeral.getPort(), other.getPort());
+ assertNotEquals(management.getPort(), reportedPorts.get(0));
+ first.close();
+ publish(first, ephemeral, null);
+ assertEquals(3, clients.constructed().size());
+ second.close();
+ explicit.close();
+ } finally {
+ ephemeral.stop();
+ other.stop();
+ fixed.stop();
+ management.stop();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private List<ShenyuWebsocketClient> clientList(final
WebsocketSyncDataService service) throws Exception {
+ Field field =
WebsocketSyncDataService.class.getDeclaredField("clients");
+ field.setAccessible(true);
+ return (List<ShenyuWebsocketClient>) field.get(service);
+ }
+
+ private void publish(final WebsocketSyncDataService service, final
WebServer server, final String namespace) {
+ ServletWebServerApplicationContext applicationContext =
mock(ServletWebServerApplicationContext.class);
+ when(applicationContext.getServerNamespace()).thenReturn(namespace);
+ service.onWebServerInitialized(new
ServletWebServerInitializedEvent(server, applicationContext));
+ }
+
+ private WebsocketSyncDataService service() {
+ WebsocketConfig config = new WebsocketConfig();
+
config.setUrls(Collections.singletonList("ws://127.0.0.1:1/websocket"));
+ ShenyuConfig shenyuConfig = new ShenyuConfig();
+ shenyuConfig.setNamespace("shenyu");
+ return new WebsocketSyncDataService(config, shenyuConfig,
mock(PluginDataSubscriber.class),
+ Collections.emptyList(), Collections.emptyList(),
Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
+ }
+
+ private WebServer start(final int port) {
+ WebServer server = new
TomcatServletWebServerFactory(port).getWebServer();
+ server.start();
+ return server;
+ }
+}
diff --git
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketMasterRedirectTest.java
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketMasterRedirectTest.java
index 922085d44e..80f71ebc84 100644
---
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketMasterRedirectTest.java
+++
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketMasterRedirectTest.java
@@ -36,7 +36,9 @@ import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
-import org.springframework.boot.autoconfigure.web.ServerProperties;
+import
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
+import
org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent;
+import org.springframework.boot.web.server.WebServer;
import java.lang.reflect.Field;
import java.net.InetSocketAddress;
@@ -208,15 +210,18 @@ class WebsocketMasterRedirectTest {
config.setAllowOrigin("https://gateway.example");
ShenyuConfig shenyu = new ShenyuConfig();
shenyu.setNamespace("cluster-test");
- ServerProperties properties = new ServerProperties();
- properties.setPort(9195);
PluginDataSubscriber subscriber = mock(PluginDataSubscriber.class);
doAnswer(invocation -> {
applied.countDown();
return null;
}).when(subscriber).onSubscribe(any());
service = new WebsocketSyncDataService(config, shenyu, subscriber,
Collections.emptyList(), Collections.emptyList(),
- Collections.emptyList(), Collections.emptyList(),
Collections.emptyList(), properties);
+ Collections.emptyList(), Collections.emptyList(),
Collections.emptyList());
+ WebServer webServer = mock(WebServer.class);
+ when(webServer.getPort()).thenReturn(9195);
+ ServletWebServerApplicationContext applicationContext =
mock(ServletWebServerApplicationContext.class);
+ when(applicationContext.getServerNamespace()).thenReturn(null);
+ service.onWebServerInitialized(new
ServletWebServerInitializedEvent(webServer, applicationContext));
}
private TestServer startServer(final boolean master) throws
InterruptedException {
diff --git
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketSyncDataServiceTest.java
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketSyncDataServiceTest.java
index 92428f9e31..e1b2cd99d4 100644
---
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketSyncDataServiceTest.java
+++
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/WebsocketSyncDataServiceTest.java
@@ -32,7 +32,6 @@ import
org.apache.shenyu.sync.data.api.ProxySelectorDataSubscriber;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.mockito.MockedStatic;
-import org.springframework.boot.autoconfigure.web.ServerProperties;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@@ -150,8 +149,7 @@ public final class WebsocketSyncDataServiceTest {
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList(),
- Collections.emptyList(),
- mock(ServerProperties.class));
+ Collections.emptyList());
try {
Field timerTaskField =
WebsocketSyncDataService.class.getDeclaredField("timerTask");
timerTaskField.setAccessible(true);
@@ -178,8 +176,7 @@ public final class WebsocketSyncDataServiceTest {
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList(),
- Collections.emptyList(),
- mock(ServerProperties.class));
+ Collections.emptyList());
Field clientsField =
WebsocketSyncDataService.class.getDeclaredField("clients");
clientsField.setAccessible(true);
List<ShenyuWebsocketClient> clients = (List<ShenyuWebsocketClient>)
clientsField.get(service);
@@ -187,6 +184,9 @@ public final class WebsocketSyncDataServiceTest {
ShenyuWebsocketClient closedClient = mock(ShenyuWebsocketClient.class);
when(closedClient.isOpen()).thenReturn(false);
clients.add(closedClient);
+ Field boundPort =
WebsocketSyncDataService.class.getDeclaredField("boundPort");
+ boundPort.setAccessible(true);
+ boundPort.setInt(service, 9195);
try {
Method masterCheck =
WebsocketSyncDataService.class.getDeclaredMethod("masterCheck");
@@ -230,7 +230,6 @@ public final class WebsocketSyncDataServiceTest {
Collections.<AuthDataSubscriber>emptyList(),
Collections.<ProxySelectorDataSubscriber>emptyList(),
Collections.<DiscoveryUpstreamDataSubscriber>emptyList(),
- Collections.<AiProxyApiKeyDataSubscriber>emptyList(),
- mock(ServerProperties.class));
+ Collections.<AiProxyApiKeyDataSubscriber>emptyList());
}
}