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