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 e45eb0ed3d fix(apache-dubbo): cache parsed selector upstreams across
requests (#7348)
e45eb0ed3d is described below
commit e45eb0ed3dd0186d2e4bd26186f922c84b6b9d70
Author: BobSong <[email protected]>
AuthorDate: Wed Sep 30 09:40:35 2026 +0800
fix(apache-dubbo): cache parsed selector upstreams across requests (#7348)
Co-authored-by: BobSong-dev <[email protected]>
Co-authored-by: aias00 <[email protected]>
---
.../apache/dubbo/cache/ApacheDubboConfigCache.java | 52 ++++++++++++++++++
.../dubbo/proxy/ApacheDubboProxyService.java | 5 +-
.../dubbo/cache/ApacheDubboConfigCacheTest.java | 64 ++++++++++++++++++++++
3 files changed, 117 insertions(+), 4 deletions(-)
diff --git
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-apache-dubbo/src/main/java/org/apache/shenyu/plugin/apache/dubbo/cache/ApacheDubboConfigCache.java
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-apache-dubbo/src/main/java/org/apache/shenyu/plugin/apache/dubbo/cache/ApacheDubboConfigCache.java
index 6ffbd5dfd6..b058798985 100644
---
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-apache-dubbo/src/main/java/org/apache/shenyu/plugin/apache/dubbo/cache/ApacheDubboConfigCache.java
+++
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-apache-dubbo/src/main/java/org/apache/shenyu/plugin/apache/dubbo/cache/ApacheDubboConfigCache.java
@@ -17,6 +17,7 @@
package org.apache.shenyu.plugin.apache.dubbo.cache;
+import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
@@ -24,7 +25,9 @@ import com.google.common.cache.RemovalListener;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -46,11 +49,13 @@ import org.apache.dubbo.rpc.service.GenericService;
import org.apache.shenyu.common.constant.Constants;
import org.apache.shenyu.common.dto.MetaData;
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.dto.convert.rule.impl.DubboRuleHandle;
import org.apache.shenyu.common.dto.convert.selector.DubboUpstream;
import org.apache.shenyu.common.exception.ShenyuException;
import org.apache.shenyu.common.utils.DigestUtils;
+import org.apache.shenyu.common.utils.GsonUtils;
import org.apache.shenyu.plugin.dubbo.common.cache.DubboConfigCache;
import org.apache.shenyu.plugin.dubbo.common.cache.DubboMethodParam;
import org.apache.shenyu.plugin.dubbo.common.cache.DubboParam;
@@ -96,6 +101,10 @@ public final class ApacheDubboConfigCache extends
DubboConfigCache {
}
});
+ private final Cache<String, CachedUpstreams> upstreamCache =
CacheBuilder.newBuilder()
+ .maximumSize(Constants.CACHE_MAX_COUNT)
+ .build();
+
/**
* Gets instance.
*
@@ -105,6 +114,33 @@ public final class ApacheDubboConfigCache extends
DubboConfigCache {
return ApplicationConfigCacheInstance.INSTANCE;
}
+ /**
+ * Return the usable upstream list carried by the selector handle, reusing
the cached
+ * parse while the handle stays unchanged. Dubbo routing runs on every
request, so the
+ * JSON deserialization must not be repeated per request; entries are
keyed by selector
+ * id and validated against the exact handle string, which makes stale
reads impossible
+ * without an invalidation hook.
+ *
+ * @param selectorData the selector carrying the serialized upstream list
+ * @return the filtered upstream list, empty when the handle holds no
usable upstream
+ */
+ public List<DubboUpstream> getOrParseUpstreams(final SelectorData
selectorData) {
+ if (Objects.isNull(selectorData) ||
StringUtils.isBlank(selectorData.getHandle())) {
+ return Collections.emptyList();
+ }
+ final CachedUpstreams cached =
upstreamCache.getIfPresent(selectorData.getId());
+ if (Objects.nonNull(cached) && Objects.equals(cached.handle,
selectorData.getHandle())) {
+ return cached.upstreams;
+ }
+ final List<DubboUpstream> parsed =
GsonUtils.getInstance().fromList(selectorData.getHandle(), DubboUpstream.class);
+ final List<DubboUpstream> usable = CollectionUtils.isEmpty(parsed) ?
Collections.emptyList()
+ : parsed.stream()
+ .filter(u -> u.isStatus() &&
StringUtils.isNotBlank(u.getRegistry()))
+ .collect(Collectors.toList());
+ upstreamCache.put(selectorData.getId(), new
CachedUpstreams(selectorData.getHandle(), usable));
+ return usable;
+ }
+
/**
* Init.
*
@@ -577,4 +613,20 @@ public final class ApacheDubboConfigCache extends
DubboConfigCache {
}
}
+
+ /**
+ * Cached parse result of one selector handle. Keeps the exact handle
string so a
+ * changed selector handle invalidates the entry without an explicit hook.
+ */
+ private static final class CachedUpstreams {
+
+ private final String handle;
+
+ private final List<DubboUpstream> upstreams;
+
+ private CachedUpstreams(final String handle, final List<DubboUpstream>
upstreams) {
+ this.handle = handle;
+ this.upstreams = upstreams;
+ }
+ }
}
diff --git
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-apache-dubbo/src/main/java/org/apache/shenyu/plugin/apache/dubbo/proxy/ApacheDubboProxyService.java
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-apache-dubbo/src/main/java/org/apache/shenyu/plugin/apache/dubbo/proxy/ApacheDubboProxyService.java
index 0634ab88f8..3dd5401f48 100644
---
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-apache-dubbo/src/main/java/org/apache/shenyu/plugin/apache/dubbo/proxy/ApacheDubboProxyService.java
+++
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-apache-dubbo/src/main/java/org/apache/shenyu/plugin/apache/dubbo/proxy/ApacheDubboProxyService.java
@@ -34,7 +34,6 @@ import
org.apache.shenyu.common.dto.convert.selector.DubboUpstream;
import org.apache.shenyu.common.enums.LoadBalanceEnum;
import org.apache.shenyu.common.enums.ResultEnum;
import org.apache.shenyu.common.exception.ShenyuException;
-import org.apache.shenyu.common.utils.GsonUtils;
import org.apache.shenyu.common.utils.JsonUtils;
import org.apache.shenyu.common.utils.ParamCheckUtils;
import org.apache.shenyu.loadbalancer.entity.Upstream;
@@ -126,9 +125,7 @@ public class ApacheDubboProxyService {
namespace =
exchange.getRequest().getHeaders().get(Constants.NAMESPACE).get(0);
}
- List<DubboUpstream> dubboUpstreams =
GsonUtils.getInstance().fromList(selectorData.getHandle(), DubboUpstream.class);
- dubboUpstreams = CollectionUtils.isEmpty(dubboUpstreams) ? null
- : dubboUpstreams.stream().filter(u -> u.isStatus() &&
StringUtils.isNotBlank(u.getRegistry())).collect(Collectors.toList());
+ List<DubboUpstream> dubboUpstreams =
ApacheDubboConfigCache.getInstance().getOrParseUpstreams(selectorData);
// if dubboUpstreams is empty, use default plugin config
if (CollectionUtils.isEmpty(dubboUpstreams)) {
referenceKey = StringUtils.isNotBlank(namespace) ? namespace +
Constants.COLONS + referenceKey : referenceKey;
diff --git
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-apache-dubbo/src/test/java/org/apache/shenyu/plugin/apache/dubbo/cache/ApacheDubboConfigCacheTest.java
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-apache-dubbo/src/test/java/org/apache/shenyu/plugin/apache/dubbo/cache/ApacheDubboConfigCacheTest.java
index b11af07d5b..7280e31034 100644
---
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-apache-dubbo/src/test/java/org/apache/shenyu/plugin/apache/dubbo/cache/ApacheDubboConfigCacheTest.java
+++
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-dubbo/shenyu-plugin-apache-dubbo/src/test/java/org/apache/shenyu/plugin/apache/dubbo/cache/ApacheDubboConfigCacheTest.java
@@ -22,6 +22,7 @@ import org.apache.dubbo.config.ReferenceConfig;
import org.apache.dubbo.config.RegistryConfig;
import org.apache.dubbo.rpc.service.GenericService;
import org.apache.shenyu.common.dto.MetaData;
+import org.apache.shenyu.common.dto.SelectorData;
import org.apache.shenyu.common.dto.convert.plugin.DubboRegisterConfig;
import org.apache.shenyu.common.dto.convert.selector.DubboUpstream;
import org.apache.shenyu.common.utils.GsonUtils;
@@ -34,10 +35,13 @@ import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import java.lang.reflect.Field;
+import java.util.List;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.mock;
@@ -179,4 +183,64 @@ public final class ApacheDubboConfigCacheTest {
field.setAccessible(true);
return (LoadingCache<String, ReferenceConfig<GenericService>>)
field.get(apacheDubboConfigCache);
}
+
+ @Test
+ public void testGetOrParseUpstreamsFiltersUnusableEntries() {
+ DubboUpstream active =
DubboUpstream.builder().protocol("dubbo").status(true).build();
+ active.setRegistry("dubbo://127.0.0.1:20880");
+ DubboUpstream disabled =
DubboUpstream.builder().protocol("dubbo").status(false).build();
+ disabled.setRegistry("dubbo://127.0.0.1:20881");
+ String handle = GsonUtils.getInstance().toJson(List.of(active,
disabled));
+ SelectorData selectorData = new SelectorData();
+ selectorData.setId("selector-1");
+ selectorData.setHandle(handle);
+
+ List<DubboUpstream> result =
apacheDubboConfigCache.getOrParseUpstreams(selectorData);
+
+ assertEquals(1, result.size());
+ assertTrue(result.get(0).isStatus());
+ }
+
+ @Test
+ public void testGetOrParseUpstreamsReusesParseWhileHandleUnchanged() {
+ DubboUpstream active =
DubboUpstream.builder().protocol("dubbo").status(true).build();
+ active.setRegistry("dubbo://127.0.0.1:20880");
+ SelectorData selectorData = new SelectorData();
+ selectorData.setId("selector-2");
+
selectorData.setHandle(GsonUtils.getInstance().toJson(List.of(active)));
+
+ List<DubboUpstream> first =
apacheDubboConfigCache.getOrParseUpstreams(selectorData);
+ List<DubboUpstream> second =
apacheDubboConfigCache.getOrParseUpstreams(selectorData);
+
+ assertSame(first, second);
+ }
+
+ @Test
+ public void testGetOrParseUpstreamsReparsesWhenHandleChanges() {
+ DubboUpstream first =
DubboUpstream.builder().protocol("dubbo").status(true).build();
+ first.setRegistry("dubbo://127.0.0.1:20880");
+ SelectorData selectorData = new SelectorData();
+ selectorData.setId("selector-3");
+ selectorData.setHandle(GsonUtils.getInstance().toJson(List.of(first)));
+ assertEquals("dubbo://127.0.0.1:20880",
apacheDubboConfigCache.getOrParseUpstreams(selectorData).get(0).getRegistry());
+
+ DubboUpstream second =
DubboUpstream.builder().protocol("dubbo").status(true).build();
+ second.setRegistry("dubbo://127.0.0.2:20880");
+
selectorData.setHandle(GsonUtils.getInstance().toJson(List.of(second)));
+ List<DubboUpstream> reparsed =
apacheDubboConfigCache.getOrParseUpstreams(selectorData);
+
+ assertEquals(1, reparsed.size());
+ assertEquals("dubbo://127.0.0.2:20880", reparsed.get(0).getRegistry());
+ }
+
+ @Test
+ public void testGetOrParseUpstreamsBlankHandleReturnsEmpty() {
+ SelectorData selectorData = new SelectorData();
+ selectorData.setId("selector-4");
+ selectorData.setHandle(null);
+
+
assertTrue(apacheDubboConfigCache.getOrParseUpstreams(selectorData).isEmpty());
+ assertTrue(apacheDubboConfigCache.getOrParseUpstreams(null).isEmpty());
+
assertNotNull(apacheDubboConfigCache.getOrParseUpstreams(selectorData));
+ }
}