This is an automated email from the ASF dual-hosted git repository.
dmsolr 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 3630671e20 fix(plugin-dubbo): reconcile gray selector caches on full
handle update (#7814)
3630671e20 is described below
commit 3630671e20f7db4ef9751bba46bfa60fd1a906a5
Author: Daming <[email protected]>
AuthorDate: Mon Oct 19 11:42:06 2026 +0800
fix(plugin-dubbo): reconcile gray selector caches on full handle update
(#7814)
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.
Co-authored-by: Luke Haochao Zhuang <[email protected]>
---
.../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();