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();

Reply via email to