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 a02acfa53d fix(admin): publish recovered upstream status in discovery 
snapshots (#7792)
a02acfa53d is described below

commit a02acfa53d95228893770816ca0bafd78274fc5b
Author: redyyang <[email protected]>
AuthorDate: Tue Oct 20 12:38:36 2026 +0800

    fix(admin): publish recovered upstream status in discovery snapshots (#7792)
---
 .../admin/service/impl/UpstreamCheckService.java   |   2 +
 .../service/UpstreamCheckServiceSyncTest.java      | 146 +++++++++++++++++++++
 2 files changed, 148 insertions(+)

diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/UpstreamCheckService.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/UpstreamCheckService.java
index 9ac848563b..b7715a6fec 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/UpstreamCheckService.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/UpstreamCheckService.java
@@ -458,6 +458,8 @@ public class UpstreamCheckService {
         discoveryUpstreamDataList.forEach(upstream -> {
             LOG.info("change alive selectorId={}|url={}", selectorId, 
upstream.getUrl());
             
discoveryUpstreamService.changeStatusBySelectorIdAndUrl(selectorId, 
upstream.getUrl(), Boolean.TRUE);
+            // The snapshot was read before the database update; publish the 
same healthy status.
+            upstream.setStatus(NumberUtils.INTEGER_ZERO);
         });
         
         DiscoverySyncData discoverySyncData = new DiscoverySyncData();
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
new file mode 100644
index 0000000000..b653b25d18
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/UpstreamCheckServiceSyncTest.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.service;
+
+import org.apache.shenyu.admin.listener.DataChangedEvent;
+import org.apache.shenyu.admin.mapper.PluginMapper;
+import org.apache.shenyu.admin.mapper.SelectorConditionMapper;
+import org.apache.shenyu.admin.mapper.SelectorMapper;
+import org.apache.shenyu.admin.model.entity.PluginDO;
+import org.apache.shenyu.admin.model.entity.SelectorDO;
+import org.apache.shenyu.admin.service.converter.DivideSelectorHandleConverter;
+import org.apache.shenyu.admin.service.converter.SelectorHandleConverterFactor;
+import org.apache.shenyu.admin.service.impl.UpstreamCheckService;
+import org.apache.shenyu.common.dto.DiscoverySyncData;
+import org.apache.shenyu.common.dto.DiscoveryUpstreamData;
+import org.apache.shenyu.common.dto.convert.selector.DivideUpstream;
+import org.apache.shenyu.common.enums.ConfigGroupEnum;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.register.common.config.ShenyuRegisterCenterConfig;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.test.util.ReflectionTestUtils;
+
+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.assertTrue;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests the discovery snapshot published after upstream health changes.
+ */
+@ExtendWith(MockitoExtension.class)
+class UpstreamCheckServiceSyncTest {
+
+    private static final String SELECTOR_ID = "sync-selector";
+
+    @Mock
+    private SelectorMapper selectorMapper;
+
+    @Mock
+    private PluginMapper pluginMapper;
+
+    @Mock
+    private SelectorConditionMapper selectorConditionMapper;
+
+    @Mock
+    private ApplicationEventPublisher eventPublisher;
+
+    @Mock
+    private DiscoveryUpstreamService discoveryUpstreamService;
+
+    private UpstreamCheckService service;
+
+    @BeforeEach
+    void setUp() {
+        SelectorHandleConverterFactor converters = new 
SelectorHandleConverterFactor(
+                Collections.singletonMap(PluginEnum.DIVIDE.getName(), new 
DivideSelectorHandleConverter()));
+        service = new UpstreamCheckService(selectorMapper, eventPublisher, 
pluginMapper, selectorConditionMapper,
+                new ShenyuRegisterCenterConfig(), converters, 
discoveryUpstreamService);
+        
when(selectorMapper.selectById(SELECTOR_ID)).thenReturn(SelectorDO.builder().id(SELECTOR_ID)
+                
.selectorName("/divide").namespaceId("namespace").pluginId("divide-id").handle("[]").build());
+        when(pluginMapper.selectById("divide-id"))
+                
.thenReturn(PluginDO.builder().id("divide-id").name(PluginEnum.DIVIDE.getName()).build());
+    }
+
+    @Test
+    void testRecoveredUpstreamPublishesCurrentStatus() {
+        DiscoveryUpstreamData recovered = 
DiscoveryUpstreamData.builder().id("recovered")
+                
.url("127.0.0.1:8080").protocol("http://";).status(1).weight(80).props("{}").build();
+        DiscoveryUpstreamData offline = 
DiscoveryUpstreamData.builder().id("offline").url("127.0.0.1:8081").status(1).build();
+        
when(discoveryUpstreamService.findBySelectorId(SELECTOR_ID)).thenReturn(new 
ArrayList<>(List.of(recovered, offline)));
+        DivideUpstream alive = 
DivideUpstream.builder().upstreamUrl(recovered.getUrl()).protocol("http://";).status(true).build();
+
+        ReflectionTestUtils.invokeMethod(service, "updateSelectorHandler", 
SELECTOR_ID, Collections.singletonList(alive));
+
+        
verify(discoveryUpstreamService).changeStatusBySelectorIdAndUrl(SELECTOR_ID, 
recovered.getUrl(), Boolean.TRUE);
+        DiscoverySyncData snapshot = publishedSnapshot();
+        assertEquals(1, snapshot.getUpstreamDataList().size());
+        DiscoveryUpstreamData upstream = snapshot.getUpstreamDataList().get(0);
+        assertEquals(0, upstream.getStatus(), "The published status must match 
the database update");
+        assertEquals("recovered", upstream.getId());
+        assertEquals(80, upstream.getWeight());
+        assertEquals("{}", upstream.getProps());
+    }
+
+    @Test
+    void testAllOfflinePublishesEmptySnapshot() {
+        DiscoveryUpstreamData offline = 
DiscoveryUpstreamData.builder().url("127.0.0.1:8080").status(1).build();
+        
when(discoveryUpstreamService.findBySelectorId(SELECTOR_ID)).thenReturn(new 
ArrayList<>(Collections.singletonList(offline)));
+
+        ReflectionTestUtils.invokeMethod(service, "updateSelectorHandler", 
SELECTOR_ID, Collections.emptyList());
+
+        assertTrue(publishedSnapshot().getUpstreamDataList().isEmpty());
+    }
+
+    @Test
+    void testUpstreamWithoutDiscoveryRecordPublishesHealthyStatus() {
+        DivideUpstream alive = 
DivideUpstream.builder().upstreamUrl("127.0.0.1:8080").protocol("http://";).status(true).build();
+
+        ReflectionTestUtils.invokeMethod(service, "updateSelectorHandler", 
SELECTOR_ID, Collections.singletonList(alive));
+
+        DiscoverySyncData snapshot = publishedSnapshot();
+        assertEquals(1, snapshot.getUpstreamDataList().size());
+        assertEquals(alive.getUpstreamUrl(), 
snapshot.getUpstreamDataList().get(0).getUrl());
+        assertEquals(0, snapshot.getUpstreamDataList().get(0).getStatus());
+    }
+
+    private DiscoverySyncData publishedSnapshot() {
+        ArgumentCaptor<DataChangedEvent> captor = 
ArgumentCaptor.forClass(DataChangedEvent.class);
+        verify(eventPublisher, times(2)).publishEvent(captor.capture());
+        DataChangedEvent event = captor.getAllValues().stream()
+                .filter(value -> value.getGroupKey() == 
ConfigGroupEnum.DISCOVER_UPSTREAM).findFirst().orElseThrow();
+        DiscoverySyncData snapshot = 
GsonUtils.getInstance().fromJson(GsonUtils.getInstance().toJson(event.getSource().get(0)),
 DiscoverySyncData.class);
+        assertEquals(SELECTOR_ID, snapshot.getSelectorId());
+        assertEquals("/divide", snapshot.getSelectorName());
+        assertEquals("namespace", snapshot.getNamespaceId());
+        assertEquals(PluginEnum.DIVIDE.getName(), snapshot.getPluginName());
+        return snapshot;
+    }
+}

Reply via email to