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 0e388ff338 fix(admin): notify gateways when discovery upstreams are 
deleted (#7819)
0e388ff338 is described below

commit 0e388ff33841246b44f223e59f9d9e7f16a80fb2
Author: Sean-Walker0 <[email protected]>
AuthorDate: Mon Oct 19 14:00:58 2026 +0800

    fix(admin): notify gateways when discovery upstreams are deleted (#7819)
    
    DELETE /discovery-upstream/batch removed the database rows and
    returned success without publishing the remaining upstream snapshot,
    so connected gateways kept the deleted URL as a routing candidate
    until some other change triggered a refresh. The sibling create and
    update paths already publish through fetchAll.
    
    Resolve the owning discovery handlers before deleting the rows, then
    publish the remaining upstream list for each affected handler —
    including an empty list when the last upstream was deleted.
    Regression test deletes the last upstream of a handler and asserts
    changeUpstream is invoked with the empty remaining snapshot (red on
    the previous code, which never notified the processor).
    
    Co-authored-by: Sean-Walker0 
<[email protected]>
---
 .../service/impl/DiscoveryUpstreamServiceImpl.java |  5 ++-
 .../service/DiscoveryUpstreamServiceTest.java      | 37 ++++++++++++++++++++++
 2 files changed, 41 insertions(+), 1 deletion(-)

diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java
index 82ad6f7f8d..515a6b7e63 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java
@@ -179,8 +179,11 @@ public class DiscoveryUpstreamServiceImpl implements 
DiscoveryUpstreamService {
      */
     @Override
     public String delete(final List<String> ids) {
-
+        // resolve the owning handlers before the rows disappear, so the 
gateways can be
+        // notified with the remaining upstream snapshot (including an empty 
one)
+        final List<DiscoveryUpstreamDO> deleted = 
discoveryUpstreamMapper.selectByIds(ids);
         discoveryUpstreamMapper.deleteByIds(ids);
+        
deleted.stream().map(DiscoveryUpstreamDO::getDiscoveryHandlerId).distinct().forEach(this::fetchAll);
         return ShenyuResultMessage.DELETE_SUCCESS;
     }
 
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java
index 7653058478..7f4f0e66cd 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java
@@ -52,6 +52,7 @@ import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 import java.sql.Timestamp;
@@ -64,6 +65,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.when;
 import org.springframework.test.util.ReflectionTestUtils;
@@ -147,6 +149,41 @@ public final class DiscoveryUpstreamServiceTest {
         assertEquals(ShenyuResultMessage.DELETE_SUCCESS, delete);
     }
 
+
+    @Test
+    public void testDeleteNotifiesAffectedHandlersWithRemainingUpstreams() {
+        final DiscoveryUpstreamDO upstream = new DiscoveryUpstreamDO();
+        upstream.setId("upstream-1");
+        upstream.setDiscoveryHandlerId("handler-1");
+        
when(discoveryUpstreamMapper.selectByIds(Collections.singletonList("upstream-1"))).thenReturn(Collections.singletonList(upstream));
+        when(discoveryUpstreamMapper.deleteByIds(any())).thenReturn(1);
+        // fetchAll dependencies for handler-1
+        
when(discoveryUpstreamMapper.selectByDiscoveryHandlerId("handler-1")).thenReturn(Collections.emptyList());
+        final DiscoveryHandlerDO handler = new DiscoveryHandlerDO();
+        handler.setId("handler-1");
+        
when(discoveryHandlerMapper.selectById("handler-1")).thenReturn(handler);
+        
when(proxySelectorMapper.selectByHandlerId("handler-1")).thenReturn(null);
+        final SelectorDO selector = new SelectorDO();
+        selector.setId("selector-1");
+        selector.setPluginId("plugin-1");
+        
when(selectorMapper.selectByDiscoveryHandlerId("handler-1")).thenReturn(selector);
+        final PluginDO plugin = new PluginDO();
+        plugin.setName("divide");
+        when(pluginMapper.selectById("plugin-1")).thenReturn(plugin);
+        handler.setDiscoveryId("discovery-1");
+        final DiscoveryDO discovery = new DiscoveryDO();
+        discovery.setId("discovery-1");
+        discovery.setDiscoveryType("local");
+        when(discoveryMapper.selectById("discovery-1")).thenReturn(discovery);
+        
when(discoveryProcessorHolder.chooseProcessor(anyString())).thenReturn(discoveryProcessor);
+
+        String delete = 
discoveryUpstreamService.delete(Collections.singletonList("upstream-1"));
+
+        assertEquals(ShenyuResultMessage.DELETE_SUCCESS, delete);
+        // the empty remaining snapshot must be published for the affected 
handler
+        verify(discoveryProcessor, 
Mockito.timeout(1000).times(1)).changeUpstream(any(), 
eq(Collections.emptyList()));
+    }
+
     @ParameterizedTest
     @ValueSource(strings = {"handler", "selector", "plugin", "discovery"})
     void rejectsMissingDiscoveryBindingsWithDomainErrors(final String missing) 
{

Reply via email to