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 ea28bde23e fix(admin): guard removeProxySelector when the discovery 
was removed first (#7365)
ea28bde23e is described below

commit ea28bde23ed7171b01d82b0fa9c7dcc9d6e58d1f
Author: Sean-Walker0 <[email protected]>
AuthorDate: Wed Sep 30 11:15:41 2026 +0800

    fix(admin): guard removeProxySelector when the discovery was removed first 
(#7365)
    
    * fix(admin): guard removeProxySelector when the discovery was removed first
    
    removeDiscovery drops only the discoveryServiceCache entry and leaves
    the dataChangedEventListenerCache entry (registered by
    createProxySelector) behind. removeProxySelector then finds the stale
    listener-cache key, runs its lambda, and calls
    unWatchInstances on a service that discoveryServiceCache.get just
    returned null for - so removing a proxy selector after its discovery
    was deleted throws NullPointerException and the PROXY_SELECTOR delete
    event is never published. Skip only the moot unwatch when the service
    is already gone: the listener key is still removed and the delete event
    still fires.
    
    The new test fails on current master with the exact NPE and passes
    with this change.
    
    * fix(admin): fold review suggestions into the removeProxySelector guard
    
    Per review: use Optional.ifPresent for the unwatch call to match the
    idiom of the surrounding code; drop the emptied listener-cache set so
    the map converges instead of keeping a tombstone entry; parameterize
    the test's reflective casts. The empty-set cleanup alone would break
    re-registration (getCacheKey returned the raw map entry and both
    createProxySelector variants call cacheKey.add on it), so getCacheKey
    now recreates the set via computeIfAbsent - unregistered discoveries
    are still rejected by the service null check above. The test now also
    asserts the tombstone is gone and re-registration gets a usable set.
    
    ---------
    
    Co-authored-by: Sean-Walker0 
<[email protected]>
    Co-authored-by: xiaoyu <[email protected]>
---
 .../discovery/AbstractDiscoveryProcessor.java      | 11 ++++++--
 .../discovery/DefaultDiscoveryProcessorTest.java   | 30 ++++++++++++++++++++++
 2 files changed, 39 insertions(+), 2 deletions(-)

diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/discovery/AbstractDiscoveryProcessor.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/discovery/AbstractDiscoveryProcessor.java
index 807da75826..9606b3518c 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/discovery/AbstractDiscoveryProcessor.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/discovery/AbstractDiscoveryProcessor.java
@@ -132,7 +132,13 @@ public abstract class AbstractDiscoveryProcessor 
implements DiscoveryProcessor,
         String key = 
buildProxySelectorKey(discoveryHandlerDTO.getListenerNode());
         
Optional.ofNullable(dataChangedEventListenerCache.get(discoveryHandlerDTO.getDiscoveryId())).ifPresent(cacheKey
 -> {
             cacheKey.remove(key);
-            shenyuDiscoveryService.unWatchInstances(key);
+            if (cacheKey.isEmpty()) {
+                // converge the map instead of leaving an empty tombstone set 
behind forever
+                
dataChangedEventListenerCache.remove(discoveryHandlerDTO.getDiscoveryId());
+            }
+            // removeDiscovery drops the service entry but leaves the 
listener-cache key behind;
+            // with the service already closed there is nothing to unwatch, 
but the delete event must still fire
+            Optional.ofNullable(shenyuDiscoveryService).ifPresent(service -> 
service.unWatchInstances(key));
             DataChangedEvent dataChangedEvent = new 
DataChangedEvent(ConfigGroupEnum.PROXY_SELECTOR, DataEventTypeEnum.DELETE,
                     
Collections.singletonList(DiscoveryTransfer.INSTANCE.mapToData(proxySelectorDTO)));
             eventPublisher.publishEvent(dataChangedEvent);
@@ -286,7 +292,8 @@ public abstract class AbstractDiscoveryProcessor implements 
DiscoveryProcessor,
      * @return set
      */
     public Set<String> getCacheKey(final String discoveryId) {
-        return dataChangedEventListenerCache.get(discoveryId);
+        // computeIfAbsent keeps re-registration safe after 
removeProxySelector dropped an emptied set
+        return dataChangedEventListenerCache.computeIfAbsent(discoveryId, k -> 
new HashSet<>());
     }
 
     /**
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/discovery/DefaultDiscoveryProcessorTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/discovery/DefaultDiscoveryProcessorTest.java
index 819cce7028..778dff6b74 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/discovery/DefaultDiscoveryProcessorTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/discovery/DefaultDiscoveryProcessorTest.java
@@ -45,6 +45,7 @@ import org.springframework.context.ApplicationEventPublisher;
 
 import java.lang.reflect.Field;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -53,6 +54,8 @@ import java.util.Properties;
 import java.util.Set;
 
 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.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doNothing;
@@ -199,4 +202,31 @@ public class DefaultDiscoveryProcessorTest {
 
     }
 
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testRemoveProxySelectorAfterDiscoveryRemoved() throws 
NoSuchFieldException, IllegalAccessException {
+        defaultDiscoveryProcessor.setApplicationEventPublisher(eventPublisher);
+        // simulate removeDiscovery: it drops only the discoveryServiceCache 
entry and leaves the
+        // listener-cache key registered by createProxySelector behind
+        final Field serviceCacheField = 
defaultDiscoveryProcessor.getClass().getSuperclass().getDeclaredField("discoveryServiceCache");
+        serviceCacheField.setAccessible(true);
+        Map<String, ShenyuInstanceRegisterRepository> serviceCache = 
(Map<String, ShenyuInstanceRegisterRepository>) 
serviceCacheField.get(defaultDiscoveryProcessor);
+        serviceCache.remove("id");
+        final Field listenerCacheField = 
defaultDiscoveryProcessor.getClass().getSuperclass().getDeclaredField("dataChangedEventListenerCache");
+        listenerCacheField.setAccessible(true);
+        Map<String, Set<String>> listenerCache = (Map<String, Set<String>>) 
listenerCacheField.get(defaultDiscoveryProcessor);
+        listenerCache.put("id", new 
HashSet<>(Collections.singleton("/shenyu/discovery")));
+
+        
doNothing().when(eventPublisher).publishEvent(any(DataChangedEvent.class));
+        DiscoveryHandlerDTO discoveryHandlerDTO = new DiscoveryHandlerDTO();
+        discoveryHandlerDTO.setDiscoveryId("id");
+        discoveryHandlerDTO.setListenerNode("/shenyu/discovery");
+        defaultDiscoveryProcessor.removeProxySelector(discoveryHandlerDTO, new 
ProxySelectorDTO());
+        verify(eventPublisher).publishEvent(any(DataChangedEvent.class));
+        // the emptied set must not linger as a tombstone entry
+        assertTrue(listenerCache.isEmpty());
+        // and re-registering the selector must get a usable set again instead 
of an NPE
+        
assertFalse(defaultDiscoveryProcessor.getCacheKey("id").contains("/shenyu/discovery"));
+    }
+
 }

Reply via email to