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 6acf4f3893 test(admin): verify crash eviction without offline
notifications (#7945)
6acf4f3893 is described below
commit 6acf4f3893bb20f3c47f8592baccfa4416416ee7
Author: redyyang <[email protected]>
AuthorDate: Fri Oct 23 11:17:24 2026 +0800
test(admin): verify crash eviction without offline notifications (#7945)
* test(admin): verify crashed upstream eviction without offline events
* test(ci): align cryptor failures and wait for usable SDK routes
* test(admin): reserve failed backend ports and assert zombie retries
---
.../service/UpstreamCheckServiceSyncTest.java | 39 ++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/UpstreamCheckServiceSyncTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/UpstreamCheckServiceSyncTest.java
index a87294f456..d9585a6bc7 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/UpstreamCheckServiceSyncTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/UpstreamCheckServiceSyncTest.java
@@ -32,6 +32,7 @@ import org.apache.shenyu.common.dto.DiscoverySyncData;
import org.apache.shenyu.common.dto.DiscoveryUpstreamData;
import org.apache.shenyu.common.dto.convert.selector.CommonUpstream;
import org.apache.shenyu.common.dto.convert.selector.DivideUpstream;
+import org.apache.shenyu.common.dto.convert.selector.ZombieUpstream;
import org.apache.shenyu.common.enums.ConfigGroupEnum;
import org.apache.shenyu.common.enums.PluginEnum;
import org.apache.shenyu.common.utils.GsonUtils;
@@ -51,6 +52,9 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.util.ReflectionTestUtils;
import java.sql.Timestamp;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -64,6 +68,7 @@ import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
@@ -124,6 +129,40 @@ class UpstreamCheckServiceSyncTest {
((Collection<?>)
ReflectionTestUtils.getField(UpstreamCheckService.class, "ZOMBIE_SET")).clear();
}
+ @Test
+ void testCrashedBackendIsEvictedWithoutAnOfflineNotification() throws
Exception {
+ try (Socket crashed = new Socket(); ServerSocket healthy = new
ServerSocket(0)) {
+ // Reserve the dead port without listening, so another process
cannot reuse it.
+ crashed.bind(new InetSocketAddress("127.0.0.1", 0));
+ String failedUrl = "127.0.0.1:" + crashed.getLocalPort();
+ assertFalse(UpstreamCheckUtils.checkUrl(failedUrl));
+ String healthyUrl = "127.0.0.1:" + healthy.getLocalPort();
+ CommonUpstream dead =
DivideUpstream.builder().upstreamUrl(failedUrl).protocol("http://").status(true).healthCheckEnabled(true).build();
+ CommonUpstream live =
DivideUpstream.builder().upstreamUrl(healthyUrl).protocol("http://").status(true).healthCheckEnabled(true).build();
+
when(discoveryUpstreamService.findBySelectorId(SELECTOR_ID)).thenReturn(List.of(
+
DiscoveryUpstreamData.builder().url(failedUrl).protocol("http://").status(0).dateCreated(new
Timestamp(1)).build(),
+
DiscoveryUpstreamData.builder().url(healthyUrl).protocol("http://").status(0).dateCreated(new
Timestamp(1)).build()));
+ ReflectionTestUtils.setField(service, "invokeExecutor", new
ScheduledThreadPoolExecutor(2));
+ service.replace(SELECTOR_ID, List.of(dead, live));
+ Map<?, ?> upstreams = (Map<?, ?>)
ReflectionTestUtils.getField(UpstreamCheckService.class, "UPSTREAM_MAP");
+
+ ReflectionTestUtils.invokeMethod(service, "check", SELECTOR_ID,
upstreams.get(SELECTOR_ID));
+ ReflectionTestUtils.invokeMethod(service, "waitFinish");
+
+
verify(discoveryUpstreamService).changeStatusBySelectorIdAndUrl(SELECTOR_ID,
failedUrl, Boolean.FALSE);
+ DiscoverySyncData snapshot = publishedSnapshot();
+ assertEquals(1, snapshot.getUpstreamDataList().size());
+ assertEquals(healthyUrl,
snapshot.getUpstreamDataList().get(0).getUrl());
+ assertEquals(Collections.singletonList(live),
upstreams.get(SELECTOR_ID));
+ ArgumentCaptor<SelectorDO> selector =
ArgumentCaptor.forClass(SelectorDO.class);
+ verify(selectorMapper).updateSelective(selector.capture());
+ assertFalse(selector.getValue().getHandle().contains(failedUrl));
+ Collection<?> zombies = (Collection<?>)
ReflectionTestUtils.getField(UpstreamCheckService.class, "ZOMBIE_SET");
+
assertTrue(zombies.stream().map(ZombieUpstream.class::cast).anyMatch(zombie ->
SELECTOR_ID.equals(zombie.getSelectorId())
+ &&
failedUrl.equals(zombie.getCommonUpstream().getUpstreamUrl()) &&
zombie.getZombieCheckTimes() > 0));
+ }
+ }
+
@Test
void testUncheckedHealthyDiscoveryUpstreamIsPreserved() {
DiscoveryUpstreamData offline =
DiscoveryUpstreamData.builder().url("127.0.0.1:8080").status(1).build();