This is an automated email from the ASF dual-hosted git repository.

dmsolr pushed a commit to branch fix/issue-7791-dubbo-gray-selector-reconcile
in repository https://gitbox.apache.org/repos/asf/shenyu.git

commit b59e03738390436ab065587f6b5e581fc4a47597
Author: Luke Haochao Zhuang <[email protected]>
AuthorDate: Sun Oct 18 18:33:57 2026 +0800

    fix(plugin-dubbo): reconcile gray selector caches on full handle update 
(#7791)
    
    A full selector-handle update did not reconcile the Dubbo gray routing 
caches with the latest configured gray targets:
    
    - An empty handle ([]) returned early, leaving SELECTOR_CACHED_HANDLE and 
UpstreamCacheManager populated with the previously cached gray targets, so gray 
routing stayed active after the configuration was cleared.
    - Replacing gray target A with B updated the selector handle cache to B, 
but UpstreamCacheManager.submit merged B into the existing state without 
removing the omitted A, so A remained a routing candidate.
    
    ApacheDubboGrayLoadBalance selects candidates from UpstreamCacheManager 
whenever the selector handle cache is nonempty, so the retained targets kept 
receiving traffic.
    
    Treat the full handle as the authoritative gray target set: clear the 
selector's stale upstream state before submitting the new gray list so omitted 
targets are removed from both the routing and health-check caches, and clear 
the gray caches for an empty/non-gray handle without touching unrelated rule 
configuration. The shared manager's incremental submit semantics are preserved 
for other callers.
---
 .../handler/AbstractDubboPluginDataHandler.java    | 28 +++++++++----
 .../AbstractDubboPluginDataHandlerTest.java        | 48 ++++++++++++++++++++++
 2 files changed, 67 insertions(+), 9 deletions(-)

diff --git 
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-dubbo-common/src/main/java/org/apache/shenyu/plugin/dubbo/common/handler/AbstractDubboPluginDataHandler.java
 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-dubbo-common/src/main/java/org/apache/shenyu/plugin/dubbo/common/handler/AbstractDubboPluginDataHandler.java
index 0eaae274bc..101765d05c 100644
--- 
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-dubbo-common/src/main/java/org/apache/shenyu/plugin/dubbo/common/handler/AbstractDubboPluginDataHandler.java
+++ 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-dubbo-common/src/main/java/org/apache/shenyu/plugin/dubbo/common/handler/AbstractDubboPluginDataHandler.java
@@ -80,22 +80,32 @@ public abstract class AbstractDubboPluginDataHandler 
implements PluginDataHandle
         if (!selectorData.getContinued()) {
             
RULE_CACHED_HANDLE.get().cachedHandle(CacheKeyUtils.INST.getKey(selectorData.getId(),
 Constants.DEFAULT_RULE), DubboRuleHandle.newInstance());
         }
-        List<DubboUpstream> dubboUpstreams = 
GsonUtils.getInstance().fromList(selectorData.getHandle(), DubboUpstream.class);
-        if (CollectionUtils.isEmpty(dubboUpstreams)) {
+        final String selectorId = selectorData.getId();
+        if (StringUtils.isBlank(selectorId)) {
+            // a selector without an identity has no gray routing caches to 
reconcile
             return;
         }
+        List<DubboUpstream> dubboUpstreams = 
GsonUtils.getInstance().fromList(selectorData.getHandle(), DubboUpstream.class);
         List<DubboUpstream> graySelectorHandle = new ArrayList<>();
-        for (DubboUpstream each : dubboUpstreams) {
-            if (StringUtils.isNotBlank(each.getUpstreamUrl()) && 
Objects.nonNull(each.isGray()) && Boolean.TRUE.equals(each.isGray())) {
-                graySelectorHandle.add(each);
+        if (CollectionUtils.isNotEmpty(dubboUpstreams)) {
+            for (DubboUpstream each : dubboUpstreams) {
+                if (StringUtils.isNotBlank(each.getUpstreamUrl()) && 
Objects.nonNull(each.isGray()) && Boolean.TRUE.equals(each.isGray())) {
+                    graySelectorHandle.add(each);
+                }
             }
         }
         if (CollectionUtils.isNotEmpty(graySelectorHandle)) {
-            SELECTOR_CACHED_HANDLE.get().cachedHandle(selectorData.getId(), 
graySelectorHandle);
-            UpstreamCacheManager.getInstance().submit(selectorData.getId(), 
convertUpstreamList(graySelectorHandle));
+            // A full selector-handle update must reconcile the gray routing 
candidates with the latest gray
+            // targets. Clear the selector's stale upstream state first so 
that omitted targets are removed from
+            // both the routing and health-check caches, instead of being 
merged and retained by submit.
+            UpstreamCacheManager.getInstance().removeByKey(selectorId);
+            SELECTOR_CACHED_HANDLE.get().cachedHandle(selectorId, 
graySelectorHandle);
+            UpstreamCacheManager.getInstance().submit(selectorId, 
convertUpstreamList(graySelectorHandle));
         } else {
-            // if update gray selector is empty, remove cache
-            removeSelector(selectorData);
+            // An empty or non-gray handle is a valid configuration 
replacement: clear the gray routing caches so
+            // the removed targets stop being candidates, without touching 
unrelated rule configuration.
+            SELECTOR_CACHED_HANDLE.get().removeHandle(selectorId);
+            UpstreamCacheManager.getInstance().removeByKey(selectorId);
         }
     }
 
diff --git 
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-dubbo-common/src/test/java/org/apache/shenyu/plugin/dubbo/common/handler/AbstractDubboPluginDataHandlerTest.java
 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-dubbo-common/src/test/java/org/apache/shenyu/plugin/dubbo/common/handler/AbstractDubboPluginDataHandlerTest.java
index 01ea18db3b..24056880da 100644
--- 
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-dubbo-common/src/test/java/org/apache/shenyu/plugin/dubbo/common/handler/AbstractDubboPluginDataHandlerTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-dubbo-common/src/test/java/org/apache/shenyu/plugin/dubbo/common/handler/AbstractDubboPluginDataHandlerTest.java
@@ -22,6 +22,8 @@ import org.apache.shenyu.common.dto.RuleData;
 import org.apache.shenyu.common.dto.SelectorData;
 import org.apache.shenyu.common.dto.convert.plugin.DubboRegisterConfig;
 import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.loadbalancer.cache.UpstreamCacheManager;
+import org.apache.shenyu.loadbalancer.entity.Upstream;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -29,6 +31,8 @@ import org.mockito.junit.jupiter.MockitoExtension;
 import org.mockito.junit.jupiter.MockitoSettings;
 import org.mockito.quality.Strictness;
 
+import java.util.List;
+
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -95,6 +99,50 @@ public class AbstractDubboPluginDataHandlerTest {
         
assertNull(AbstractDubboPluginDataHandler.SELECTOR_CACHED_HANDLE.get().obtainHandle("1"));
     }
 
+    @Test
+    public void emptyHandleClearsGrayRoutingCaches() {
+        final String selectorId = "gray-empty-selector";
+        UpstreamCacheManager.getInstance().removeByKey(selectorId);
+        SelectorData selectorData = new SelectorData();
+        selectorData.setId(selectorId);
+        selectorData.setContinued(true);
+        
selectorData.setHandle("[{\"protocol\":\"dubbo://\",\"upstreamUrl\":\"127.0.0.1:20880\",\"gray\":true,"
+                + 
"\"weight\":1,\"status\":true,\"healthCheckEnabled\":false}]");
+        handler.handlerSelector(selectorData);
+        assertEquals(1, 
AbstractDubboPluginDataHandler.SELECTOR_CACHED_HANDLE.get().obtainHandle(selectorId).size());
+        assertEquals(1, 
UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selectorId).size());
+
+        // a full update to an empty handle is a valid replacement and must 
clear gray routing
+        selectorData.setHandle("[]");
+        handler.handlerSelector(selectorData);
+        
assertNull(AbstractDubboPluginDataHandler.SELECTOR_CACHED_HANDLE.get().obtainHandle(selectorId));
+        
assertNull(UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selectorId));
+        UpstreamCacheManager.getInstance().removeByKey(selectorId);
+    }
+
+    @Test
+    public void replacingGrayTargetRemovesOmittedUpstream() {
+        final String selectorId = "gray-replace-selector";
+        UpstreamCacheManager.getInstance().removeByKey(selectorId);
+        SelectorData selectorData = new SelectorData();
+        selectorData.setId(selectorId);
+        selectorData.setContinued(true);
+        
selectorData.setHandle("[{\"protocol\":\"dubbo://\",\"upstreamUrl\":\"127.0.0.1:20880\",\"gray\":true,"
+                + 
"\"weight\":1,\"status\":true,\"healthCheckEnabled\":false}]");
+        handler.handlerSelector(selectorData);
+        assertEquals(1, 
UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selectorId).size());
+
+        // replace gray target A (20880) with B (20881): A must no longer 
remain a routing candidate
+        
selectorData.setHandle("[{\"protocol\":\"dubbo://\",\"upstreamUrl\":\"127.0.0.1:20881\",\"gray\":true,"
+                + 
"\"weight\":1,\"status\":true,\"healthCheckEnabled\":false}]");
+        handler.handlerSelector(selectorData);
+        List<Upstream> candidates = 
UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selectorId);
+        assertEquals(1, candidates.size());
+        assertEquals("127.0.0.1:20881", candidates.get(0).getUrl());
+        assertEquals(1, 
AbstractDubboPluginDataHandler.SELECTOR_CACHED_HANDLE.get().obtainHandle(selectorId).size());
+        UpstreamCacheManager.getInstance().removeByKey(selectorId);
+    }
+
     @Test
     public void ruleTest() {
         RuleData ruleData = new RuleData();

Reply via email to