This is an automated email from the ASF dual-hosted git repository.
albumenj pushed a commit to branch 3.2
in repository https://gitbox.apache.org/repos/asf/dubbo.git
The following commit(s) were added to refs/heads/3.2 by this push:
new f95e659eef Feat xdsroute parse (#10956)
f95e659eef is described below
commit f95e659eeff52eeba16a94ccf792535acc530c02
Author: haoyann <[email protected]>
AuthorDate: Fri Dec 9 16:31:40 2022 +0800
Feat xdsroute parse (#10956)
Co-authored-by: Albumen Kevin <[email protected]>
---
.../cluster/router/xds/EdsEndpointListener.java | 28 ++
.../rpc/cluster/router/xds/EdsEndpointManager.java | 114 +++++++
.../cluster/router/xds/RdsRouteRuleManager.java | 116 +++++++
.../cluster/router/xds/RdsVirtualHostListener.java | 177 ++++++++++
.../cluster/router/xds/XdsRouteRuleListener.java | 30 ++
.../dubbo/rpc/cluster/router/xds/XdsRouter.java | 366 +++++++++++++++++++++
.../rpc/cluster/router/xds/XdsRouterFactory.java | 31 ++
.../router/xds/XdsScopeModelInitializer.java | 42 +++
.../rpc/cluster/router/xds/rule/ClusterWeight.java | 38 +++
.../cluster/router/xds/rule/DestinationSubset.java | 58 ++++
.../router/xds/rule/HTTPRouteDestination.java | 43 +++
.../rpc/cluster/router/xds/rule/HeaderMatcher.java | 123 +++++++
.../cluster/router/xds/rule/HttpRequestMatch.java | 41 +++
.../cluster/router/xds/rule/LongRangeMatch.java | 49 +++
.../rpc/cluster/router/xds/rule/PathMatcher.java | 74 +++++
.../rpc/cluster/router/xds/rule/XdsRouteRule.java | 41 +++
...bbo.rpc.cluster.router.state.StateRouterFactory | 1 +
...rg.apache.dubbo.rpc.model.ScopeModelInitializer | 1 +
.../cluster/router/xds/EdsEndpointManagerTest.java | 116 +++++++
.../router/xds/RdsRouteRuleManagerTest.java | 139 ++++++++
.../router/xds/RdsVirtualHostListenerTest.java | 231 +++++++++++++
.../dubbo/rpc/cluster/router/xds/XdsRouteTest.java | 363 ++++++++++++++++++++
.../cluster/router/xds/rule/HeaderMatcherTest.java | 92 ++++++
.../cluster/router/xds/rule/PathMatcherTest.java | 60 ++++
24 files changed, 2374 insertions(+)
diff --git
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointListener.java
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointListener.java
new file mode 100644
index 0000000000..0416236166
--- /dev/null
+++
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointListener.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds;
+
+import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint;
+
+import java.util.Set;
+
+
+public interface EdsEndpointListener {
+
+ void onEndPointChange(String cluster, Set<Endpoint> endpoints);
+
+}
diff --git
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManager.java
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManager.java
new file mode 100644
index 0000000000..e29f9becc8
--- /dev/null
+++
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManager.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds;
+
+import org.apache.dubbo.common.utils.CollectionUtils;
+import org.apache.dubbo.common.utils.ConcurrentHashSet;
+import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint;
+
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+
+public class EdsEndpointManager {
+
+ private static final ConcurrentHashMap<String, Set<EdsEndpointListener>>
ENDPOINT_LISTENERS = new ConcurrentHashMap<>();
+
+ private static final ConcurrentHashMap<String, Set<Endpoint>>
ENDPOINT_DATA_CACHE = new ConcurrentHashMap<>();
+
+ private static final ConcurrentHashMap<String, Consumer<Set<Endpoint>>>
EDS_LISTENERS = new ConcurrentHashMap<>();
+
+
+ public EdsEndpointManager() {
+ }
+
+ public synchronized void subscribeEds(String cluster, EdsEndpointListener
listener) {
+
+ Set<EdsEndpointListener> listeners =
ENDPOINT_LISTENERS.computeIfAbsent(cluster, key ->
+ new ConcurrentHashSet<>()
+ );
+ if (CollectionUtils.isEmpty(listeners)) {
+ doSubscribeEds(cluster);
+ }
+ listeners.add(listener);
+
+ if (ENDPOINT_DATA_CACHE.containsKey(cluster)) {
+ listener.onEndPointChange(cluster,
ENDPOINT_DATA_CACHE.get(cluster));
+ }
+ }
+
+ private void doSubscribeEds(String cluster) {
+ EDS_LISTENERS.computeIfAbsent(cluster, key -> endpoints -> {
+ notifyEndpointChange(cluster, endpoints);
+ });
+ Consumer<Set<Endpoint>> consumer = EDS_LISTENERS.get(cluster);
+
+ //todo control plane subscribe eds
+ }
+
+ public synchronized void unSubscribeEds(String cluster,
EdsEndpointListener listener) {
+ Set<EdsEndpointListener> listeners = ENDPOINT_LISTENERS.get(cluster);
+ if (CollectionUtils.isEmpty(listeners)) {
+ return;
+ }
+ listeners.remove(listener);
+ if (listeners.isEmpty()) {
+ ENDPOINT_LISTENERS.remove(cluster);
+ doUnsubscribeEds(cluster);
+ }
+ }
+
+ private void doUnsubscribeEds(String cluster) {
+ Consumer<Set<Endpoint>> consumer = EDS_LISTENERS.remove(cluster);
+
+ if (consumer != null) {
+
+ //todo control plane unsubscribe eds
+ }
+ ENDPOINT_DATA_CACHE.remove(cluster);
+ }
+
+
+ public void notifyEndpointChange(String cluster, Set<Endpoint> endpoints) {
+
+ ENDPOINT_DATA_CACHE.put(cluster, endpoints);
+
+ Set<EdsEndpointListener> listeners = ENDPOINT_LISTENERS.get(cluster);
+ if (CollectionUtils.isEmpty(listeners)) {
+ return;
+ }
+ for (EdsEndpointListener listener : listeners) {
+ listener.onEndPointChange(cluster, endpoints);
+ }
+ }
+
+ // for test
+ static ConcurrentHashMap<String, Set<EdsEndpointListener>>
getEndpointListeners() {
+ return ENDPOINT_LISTENERS;
+ }
+
+ // for test
+ static ConcurrentHashMap<String, Set<Endpoint>> getEndpointDataCache() {
+ return ENDPOINT_DATA_CACHE;
+ }
+
+ // for test
+ static ConcurrentHashMap<String, Consumer<Set<Endpoint>>>
getEdsListeners() {
+ return EDS_LISTENERS;
+ }
+
+}
diff --git
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManager.java
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManager.java
new file mode 100644
index 0000000000..0d60c20c0f
--- /dev/null
+++
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManager.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds;
+
+import org.apache.dubbo.common.utils.CollectionUtils;
+import org.apache.dubbo.common.utils.ConcurrentHashSet;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class RdsRouteRuleManager {
+
+
+ private static final ConcurrentHashMap<String, Set<XdsRouteRuleListener>>
RULE_LISTENERS = new ConcurrentHashMap<>();
+
+ private static final ConcurrentHashMap<String, List<XdsRouteRule>>
ROUTE_DATA_CACHE = new ConcurrentHashMap<>();
+
+ private static final ConcurrentHashMap<String, RdsVirtualHostListener>
RDS_LISTENERS = new ConcurrentHashMap<>();
+
+ public RdsRouteRuleManager() {
+ }
+
+ public synchronized void subscribeRds(String domain, XdsRouteRuleListener
listener) {
+
+ Set<XdsRouteRuleListener> listeners =
RULE_LISTENERS.computeIfAbsent(domain, key ->
+ new ConcurrentHashSet<>()
+ );
+ if (CollectionUtils.isEmpty(listeners)) {
+ doSubscribeRds(domain);
+ }
+ listeners.add(listener);
+
+ if (ROUTE_DATA_CACHE.containsKey(domain)) {
+ listener.onRuleChange(domain, ROUTE_DATA_CACHE.get(domain));
+ }
+ }
+
+ private void doSubscribeRds(String domain) {
+ RDS_LISTENERS.computeIfAbsent(domain, key -> new
RdsVirtualHostListener(domain, this));
+ RdsVirtualHostListener rdsVirtualHostListener =
RDS_LISTENERS.get(domain);
+ // todo request control plane subscribe rds
+ }
+
+ public synchronized void unSubscribeRds(String domain,
XdsRouteRuleListener listener) {
+ Set<XdsRouteRuleListener> listeners = RULE_LISTENERS.get(domain);
+ if (CollectionUtils.isEmpty(listeners)) {
+ return;
+ }
+ listeners.remove(listener);
+ if (listeners.isEmpty()) {
+ RULE_LISTENERS.remove(domain);
+ doUnsubscribeRds(domain);
+ }
+ }
+
+ private void doUnsubscribeRds(String domain) {
+ RdsVirtualHostListener rdsVirtualHostListener =
RDS_LISTENERS.remove(domain);
+
+ if (rdsVirtualHostListener != null) {
+
+ // todo request control plane unsubscribe rds
+ }
+ ROUTE_DATA_CACHE.remove(domain);
+ }
+
+
+ public void notifyRuleChange(String domain, List<XdsRouteRule>
xdsRouteRules) {
+
+ ROUTE_DATA_CACHE.put(domain, xdsRouteRules);
+
+ Set<XdsRouteRuleListener> listeners = RULE_LISTENERS.get(domain);
+ if (CollectionUtils.isEmpty(listeners)) {
+ return;
+ }
+ boolean empty = CollectionUtils.isEmpty(xdsRouteRules);
+ for (XdsRouteRuleListener listener : listeners) {
+ if (empty) {
+ listener.clearRule(domain);
+ } else {
+ listener.onRuleChange(domain, xdsRouteRules);
+ }
+ }
+ }
+
+ // for test
+ static ConcurrentHashMap<String, Set<XdsRouteRuleListener>>
getRuleListeners() {
+ return RULE_LISTENERS;
+ }
+
+ // for test
+ static ConcurrentHashMap<String, List<XdsRouteRule>> getRouteDataCache() {
+ return ROUTE_DATA_CACHE;
+ }
+
+ // for test
+ static ConcurrentHashMap<String, RdsVirtualHostListener> getRdsListeners()
{
+ return RDS_LISTENERS;
+ }
+
+}
diff --git
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListener.java
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListener.java
new file mode 100644
index 0000000000..b29e21626a
--- /dev/null
+++
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListener.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds;
+
+import org.apache.dubbo.common.logger.Logger;
+import org.apache.dubbo.common.logger.LoggerFactory;
+import org.apache.dubbo.common.utils.CollectionUtils;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.ClusterWeight;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.HTTPRouteDestination;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.HeaderMatcher;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.HttpRequestMatch;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.LongRangeMatch;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.PathMatcher;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule;
+
+import io.envoyproxy.envoy.config.route.v3.Route;
+import io.envoyproxy.envoy.config.route.v3.RouteAction;
+import io.envoyproxy.envoy.config.route.v3.RouteMatch;
+import io.envoyproxy.envoy.config.route.v3.VirtualHost;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class RdsVirtualHostListener {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(RdsVirtualHostListener.class);
+
+ private final String domain;
+
+ private final RdsRouteRuleManager routeRuleManager;
+
+
+ public RdsVirtualHostListener(String domain, RdsRouteRuleManager
routeRuleManager) {
+ this.domain = domain;
+ this.routeRuleManager = routeRuleManager;
+ }
+
+ public void parseVirtualHost(VirtualHost virtualHost) {
+ if (virtualHost == null ||
CollectionUtils.isEmpty(virtualHost.getRoutesList())) {
+ // post empty
+ routeRuleManager.notifyRuleChange(domain, new ArrayList<>());
+ return;
+ }
+ try {
+ List<XdsRouteRule> xdsRouteRules =
virtualHost.getRoutesList().stream().map(route -> {
+ if (route.getMatch().getQueryParametersCount() != 0) {
+ return null;
+ }
+ HttpRequestMatch match = parseMatch(route.getMatch());
+ HTTPRouteDestination action = parseAction(route);
+ return new XdsRouteRule(match, action);
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+ // post rules
+ routeRuleManager.notifyRuleChange(domain, xdsRouteRules);
+ } catch (Exception e) {
+ LOGGER.error("parse domain: " + domain + " xds VirtualHost error",
e);
+ }
+
+ }
+
+ private HttpRequestMatch parseMatch(RouteMatch match) {
+ PathMatcher pathMatcher = parsePathMatch(match);
+ List<HeaderMatcher> headerMatchers = parseHeadMatch(match);
+ return new HttpRequestMatch(pathMatcher, headerMatchers);
+ }
+
+ private PathMatcher parsePathMatch(RouteMatch match) {
+ boolean caseSensitive = match.getCaseSensitive().getValue();
+ PathMatcher pathMatcher = new PathMatcher();
+ pathMatcher.setCaseSensitive(caseSensitive);
+ switch (match.getPathSpecifierCase()) {
+ case PREFIX:
+ pathMatcher.setPrefix(match.getPrefix());
+ return pathMatcher;
+ case PATH:
+ pathMatcher.setPath(match.getPath());
+ return pathMatcher;
+ case SAFE_REGEX:
+ String regex = match.getSafeRegex().getRegex();
+ pathMatcher.setRegex(regex);
+ return pathMatcher;
+ case PATHSPECIFIER_NOT_SET:
+ return null;
+ default:
+ throw new IllegalArgumentException("Path specifier is not
expect");
+ }
+ }
+
+ private List<HeaderMatcher> parseHeadMatch(RouteMatch routeMatch) {
+ List<HeaderMatcher> headerMatchers = new ArrayList<>();
+ List<io.envoyproxy.envoy.config.route.v3.HeaderMatcher> headersList =
routeMatch.getHeadersList();
+ for (io.envoyproxy.envoy.config.route.v3.HeaderMatcher headerMatcher :
headersList) {
+ HeaderMatcher matcher = new HeaderMatcher();
+ matcher.setName(headerMatcher.getName());
+ matcher.setInverted(headerMatcher.getInvertMatch());
+ switch (headerMatcher.getHeaderMatchSpecifierCase()) {
+ case EXACT_MATCH:
+ matcher.setExactValue(headerMatcher.getExactMatch());
+ headerMatchers.add(matcher);
+ break;
+ case SAFE_REGEX_MATCH:
+
matcher.setRegex(headerMatcher.getSafeRegexMatch().getRegex());
+ headerMatchers.add(matcher);
+ break;
+ case RANGE_MATCH:
+ LongRangeMatch rang = new LongRangeMatch();
+ rang.setStart(headerMatcher.getRangeMatch().getStart());
+ rang.setEnd(headerMatcher.getRangeMatch().getEnd());
+ matcher.setRange(rang);
+ headerMatchers.add(matcher);
+ break;
+ case PRESENT_MATCH:
+ matcher.setPresent(headerMatcher.getPresentMatch());
+ headerMatchers.add(matcher);
+ break;
+ case PREFIX_MATCH:
+ matcher.setPrefix(headerMatcher.getPrefixMatch());
+ headerMatchers.add(matcher);
+ break;
+ case SUFFIX_MATCH:
+ matcher.setSuffix(headerMatcher.getSuffixMatch());
+ headerMatchers.add(matcher);
+ break;
+ case HEADERMATCHSPECIFIER_NOT_SET:
+ default:
+ throw new IllegalArgumentException("Header specifier is
not expect");
+ }
+ }
+ return headerMatchers;
+ }
+
+ private HTTPRouteDestination parseAction(Route route) {
+ switch (route.getActionCase()) {
+ case ROUTE:
+ HTTPRouteDestination httpRouteDestination = new
HTTPRouteDestination();
+ // only support cluster and weight cluster
+ RouteAction routeAction = route.getRoute();
+ RouteAction.ClusterSpecifierCase clusterSpecifierCase =
routeAction.getClusterSpecifierCase();
+ if (clusterSpecifierCase ==
RouteAction.ClusterSpecifierCase.CLUSTER) {
+ httpRouteDestination.setCluster(routeAction.getCluster());
+ return httpRouteDestination;
+ } else if (clusterSpecifierCase ==
RouteAction.ClusterSpecifierCase.WEIGHTED_CLUSTERS) {
+ List<ClusterWeight> clusterWeights =
routeAction.getWeightedClusters().
+ getClustersList().stream()
+ .map(c -> new ClusterWeight(c.getName(),
c.getWeight().getValue()))
+ .sorted(Comparator.comparing(ClusterWeight::getWeight))
+ .collect(Collectors.toList());
+ httpRouteDestination.setWeightedClusters(clusterWeights);
+ return httpRouteDestination;
+ }
+ case REDIRECT:
+ case DIRECT_RESPONSE:
+ case FILTER_ACTION:
+ case ACTION_NOT_SET:
+ default:
+ throw new IllegalArgumentException("Cluster specifier is not
expect");
+ }
+ }
+
+}
diff --git
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteRuleListener.java
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteRuleListener.java
new file mode 100644
index 0000000000..bc2c5262ca
--- /dev/null
+++
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteRuleListener.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds;
+
+import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule;
+
+import java.util.List;
+
+public interface XdsRouteRuleListener {
+
+
+ void onRuleChange(String appName, List<XdsRouteRule> xdsRouteRules);
+
+ void clearRule(String appName);
+
+}
diff --git
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouter.java
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouter.java
new file mode 100644
index 0000000000..17a4af0f85
--- /dev/null
+++
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouter.java
@@ -0,0 +1,366 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.utils.CollectionUtils;
+import org.apache.dubbo.common.utils.ConcurrentHashSet;
+import org.apache.dubbo.common.utils.Holder;
+import org.apache.dubbo.common.utils.StringUtils;
+import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint;
+import org.apache.dubbo.rpc.Invocation;
+import org.apache.dubbo.rpc.Invoker;
+import org.apache.dubbo.rpc.RpcException;
+import org.apache.dubbo.rpc.cluster.router.RouterSnapshotNode;
+import org.apache.dubbo.rpc.cluster.router.state.AbstractStateRouter;
+import org.apache.dubbo.rpc.cluster.router.state.BitList;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.ClusterWeight;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.DestinationSubset;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.HTTPRouteDestination;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.HeaderMatcher;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.HttpRequestMatch;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.PathMatcher;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Collectors;
+
+public class XdsRouter<T> extends AbstractStateRouter<T> implements
XdsRouteRuleListener, EdsEndpointListener {
+
+ private Set<String> subscribeApplications;
+
+ private final ConcurrentHashMap<String, List<XdsRouteRule>>
xdsRouteRuleMap;
+
+ private final ConcurrentHashMap<String, DestinationSubset<T>>
destinationSubsetMap;
+
+ private final RdsRouteRuleManager rdsRouteRuleManager;
+
+ private final EdsEndpointManager edsEndpointManager;
+
+ private volatile BitList<Invoker<T>> currentInvokeList;
+
+ private static final String BINARY_HEADER_SUFFIX = "-bin";
+
+ public XdsRouter(URL url) {
+ super(url);
+ rdsRouteRuleManager =
url.getOrDefaultApplicationModel().getBeanFactory().getBean(RdsRouteRuleManager.class);
+ edsEndpointManager =
url.getOrDefaultApplicationModel().getBeanFactory().getBean(EdsEndpointManager.class);
+ subscribeApplications = new ConcurrentHashSet<>();
+ destinationSubsetMap = new ConcurrentHashMap<>();
+ xdsRouteRuleMap = new ConcurrentHashMap<>();
+ currentInvokeList = new BitList<>(new ArrayList<>());
+ }
+
+ /**
+ * @deprecated only for uts
+ */
+ protected XdsRouter(URL url, RdsRouteRuleManager rdsRouteRuleManager,
EdsEndpointManager edsEndpointManager) {
+ super(url);
+ this.rdsRouteRuleManager = rdsRouteRuleManager;
+ this.edsEndpointManager = edsEndpointManager;
+ subscribeApplications = new ConcurrentHashSet<>();
+ destinationSubsetMap = new ConcurrentHashMap<>();
+ xdsRouteRuleMap = new ConcurrentHashMap<>();
+ currentInvokeList = new BitList<>(new ArrayList<>());
+ }
+
+ @Override
+ protected BitList<Invoker<T>> doRoute(BitList<Invoker<T>> invokers, URL
url, Invocation invocation,
+ boolean needToPrintMessage,
Holder<RouterSnapshotNode<T>> nodeHolder,
+ Holder<String> messageHolder) throws
RpcException {
+ if (CollectionUtils.isEmpty(invokers)) {
+ if (needToPrintMessage) {
+ messageHolder.set("Directly Return. Reason: Invokers from
previous router is empty.");
+ }
+ return invokers;
+ }
+
+ if (CollectionUtils.isEmptyMap(xdsRouteRuleMap)) {
+ if (needToPrintMessage) {
+ messageHolder.set("Directly Return. Reason: xds route rule is
empty.");
+ }
+ return invokers;
+ }
+
+ StringBuilder stringBuilder = needToPrintMessage ? new StringBuilder()
: null;
+
+ // find match cluster
+ String matchCluster = null;
+ Set<String> appNames = subscribeApplications;
+ for (String subscribeApplication : appNames) {
+ List<XdsRouteRule> rules =
xdsRouteRuleMap.get(subscribeApplication);
+ if (CollectionUtils.isEmpty(rules)) {
+ continue;
+ }
+ for (XdsRouteRule rule : rules) {
+ String cluster = computeMatchCluster(invocation, rule);
+ if (cluster != null) {
+ matchCluster = cluster;
+ break;
+ }
+ }
+ if (matchCluster != null) {
+ if (stringBuilder != null) {
+ stringBuilder.append("Match App:
").append(subscribeApplication).append(" Cluster:
").append(matchCluster).append(' ');
+ }
+ break;
+ }
+ }
+ // not match request just return
+ if (matchCluster == null) {
+ if (needToPrintMessage) {
+ messageHolder.set("Directly Return. Reason: xds rule not
match.");
+ }
+ return invokers;
+ }
+ DestinationSubset<T> destinationSubset =
destinationSubsetMap.get(matchCluster);
+ // cluster no target provider
+ if (destinationSubset == null) {
+ if (needToPrintMessage) {
+ messageHolder.set(stringBuilder.append("no target
subset").toString());
+ }
+ return BitList.emptyList();
+ }
+ if (needToPrintMessage) {
+ messageHolder.set(stringBuilder.toString());
+ }
+ if (destinationSubset.getInvokers() == null) {
+ return BitList.emptyList();
+ }
+
+ return destinationSubset.getInvokers().and(invokers);
+ }
+
+ private String computeMatchCluster(Invocation invocation, XdsRouteRule
rule) {
+ // compute request match cluster
+ HttpRequestMatch requestMatch = rule.getMatch();
+ if (requestMatch.getPathMatcher() == null &&
CollectionUtils.isEmpty(requestMatch.getHeaderMatcherList())) {
+ return null;
+ }
+ PathMatcher pathMatcher = requestMatch.getPathMatcher();
+ if (pathMatcher != null) {
+ String path = "/" + invocation.getInvoker().getUrl().getPath() +
"/" + invocation.getMethodName();
+ if (!pathMatcher.isMatch(path)) {
+ return null;
+ }
+ }
+ List<HeaderMatcher> headerMatchers =
requestMatch.getHeaderMatcherList();
+ for (HeaderMatcher headerMatcher : headerMatchers) {
+ String headerName = headerMatcher.getName();
+ // not support byte
+ if (headerName.endsWith(BINARY_HEADER_SUFFIX)) {
+ return null;
+ }
+ String headValue = invocation.getAttachment(headerName);
+ if (!headerMatcher.match(headValue)) {
+ return null;
+ }
+ }
+ HTTPRouteDestination route = rule.getRoute();
+ if (route.getCluster() != null) {
+ return route.getCluster();
+ }
+ return computeWeightCluster(route.getWeightedClusters());
+ }
+
+ private String computeWeightCluster(List<ClusterWeight> weightedClusters) {
+ int totalWeight =
Math.max(weightedClusters.stream().mapToInt(ClusterWeight::getWeight).sum(), 1);
+ // target must greater than 0
+ // if weight is 0, the destination will not receive any traffic.
+ int target = ThreadLocalRandom.current().nextInt(1, totalWeight + 1);
+ for (ClusterWeight weightedCluster : weightedClusters) {
+ int weight = weightedCluster.getWeight();
+ target -= weight;
+ if (target <= 0) {
+ return weightedCluster.getName();
+ }
+ }
+ return null;
+ }
+
+ public void notify(BitList<Invoker<T>> invokers) {
+ BitList<Invoker<T>> invokerList = invokers == null ?
BitList.emptyList() : invokers;
+ currentInvokeList = invokerList.clone();
+
+ // compute need subscribe/unsubscribe rds application
+ Set<String> currentApplications = new HashSet<>();
+ for (Invoker<T> invoker : invokerList) {
+ String applicationName = invoker.getUrl().getRemoteApplication();
+ if (StringUtils.isNotEmpty(applicationName)) {
+ currentApplications.add(applicationName);
+ }
+ }
+
+ if (!subscribeApplications.equals(currentApplications)) {
+ synchronized (this) {
+ for (String currentApplication : currentApplications) {
+ if (!subscribeApplications.contains(currentApplication)) {
+ rdsRouteRuleManager.subscribeRds(currentApplication,
this);
+ }
+ }
+ for (String preApplication : subscribeApplications) {
+ if (!currentApplications.contains(preApplication)) {
+ rdsRouteRuleManager.unSubscribeRds(preApplication,
this);
+ }
+ }
+ subscribeApplications = currentApplications;
+ }
+ }
+
+ // update subset
+ synchronized (this) {
+ BitList<Invoker<T>> allInvokers = currentInvokeList.clone();
+ for (DestinationSubset<T> subset : destinationSubsetMap.values()) {
+ computeSubset(subset, allInvokers);
+ }
+ }
+
+ }
+
+ private void computeSubset(DestinationSubset<T> subset,
BitList<Invoker<T>> invokers) {
+ Set<Endpoint> endpoints = subset.getEndpoints();
+ List<Invoker<T>> filterInvokers = invokers.stream().filter(inv -> {
+ String host = inv.getUrl().getHost();
+ int port = inv.getUrl().getPort();
+ Optional<Endpoint> any = endpoints.stream()
+ .filter(end -> host.equals(end.getAddress()) && port ==
end.getPortValue())
+ .findAny();
+ return any.isPresent();
+ }).collect(Collectors.toList());
+ subset.setInvokers(new BitList<>(filterInvokers));
+ }
+
+ @Override
+ public synchronized void onRuleChange(String appName, List<XdsRouteRule>
xdsRouteRules) {
+ if (CollectionUtils.isEmpty(xdsRouteRules)) {
+ clearRule(appName);
+ return;
+ }
+ Set<String> oldCluster = getAllCluster();
+ xdsRouteRuleMap.put(appName, xdsRouteRules);
+ Set<String> newCluster = getAllCluster();
+ changeClusterSubscribe(oldCluster, newCluster);
+ }
+
+ private Set<String> getAllCluster() {
+ if (CollectionUtils.isEmptyMap(xdsRouteRuleMap)) {
+ return new HashSet<>();
+ }
+ Set<String> clusters = new HashSet<>();
+ xdsRouteRuleMap.forEach((appName, rules) -> {
+ for (XdsRouteRule rule : rules) {
+ HTTPRouteDestination action = rule.getRoute();
+ if (action.getCluster() != null) {
+ clusters.add(action.getCluster());
+ } else if
(CollectionUtils.isNotEmpty(action.getWeightedClusters())) {
+ for (ClusterWeight weightedCluster :
action.getWeightedClusters()) {
+ clusters.add(weightedCluster.getName());
+ }
+ }
+ }
+ });
+ return clusters;
+ }
+
+ private void changeClusterSubscribe(Set<String> oldCluster, Set<String>
newCluster) {
+ Set<String> removeSubscribe = new HashSet<>(oldCluster);
+ Set<String> addSubscribe = new HashSet<>(newCluster);
+
+ removeSubscribe.removeAll(newCluster);
+ addSubscribe.removeAll(oldCluster);
+ // remove subscribe cluster
+ for (String cluster : removeSubscribe) {
+ edsEndpointManager.unSubscribeEds(cluster, this);
+ destinationSubsetMap.remove(cluster);
+ }
+ // add subscribe cluster
+ for (String cluster : addSubscribe) {
+ destinationSubsetMap.put(cluster, new
DestinationSubset<>(cluster));
+ edsEndpointManager.subscribeEds(cluster, this);
+ }
+ }
+
+ @Override
+ public synchronized void clearRule(String appName) {
+ Set<String> oldCluster = getAllCluster();
+ List<XdsRouteRule> oldRules = xdsRouteRuleMap.remove(appName);
+ if (CollectionUtils.isEmpty(oldRules)) {
+ return;
+ }
+ Set<String> newCluster = getAllCluster();
+ changeClusterSubscribe(oldCluster, newCluster);
+ }
+
+ @Override
+ public synchronized void onEndPointChange(String cluster, Set<Endpoint>
endpoints) {
+ // find and update subset
+ DestinationSubset<T> subset = destinationSubsetMap.get(cluster);
+ if (subset == null) {
+ return;
+ }
+ subset.setEndpoints(endpoints);
+ computeSubset(subset, currentInvokeList.clone());
+ }
+
+ @Override
+ public void stop() {
+ for (String app : subscribeApplications) {
+ rdsRouteRuleManager.unSubscribeRds(app, this);
+ }
+ for (String cluster : getAllCluster()) {
+ edsEndpointManager.unSubscribeEds(cluster, this);
+ }
+ }
+
+
+ @Deprecated
+ Set<String> getSubscribeApplications() {
+ return subscribeApplications;
+ }
+
+ /**
+ * for ut only
+ */
+ @Deprecated
+ BitList<Invoker<T>> getInvokerList() {
+ return currentInvokeList;
+ }
+
+ /**
+ * for ut only
+ */
+ @Deprecated
+ ConcurrentHashMap<String, List<XdsRouteRule>> getXdsRouteRuleMap() {
+ return xdsRouteRuleMap;
+ }
+
+
+ /**
+ * for ut only
+ */
+ @Deprecated
+ ConcurrentHashMap<String, DestinationSubset<T>> getDestinationSubsetMap() {
+ return destinationSubsetMap;
+ }
+
+}
diff --git
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouterFactory.java
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouterFactory.java
new file mode 100644
index 0000000000..0e9a0c5750
--- /dev/null
+++
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouterFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.rpc.cluster.router.state.StateRouter;
+import org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory;
+
+@Activate(order = 100)
+public class XdsRouterFactory implements StateRouterFactory {
+
+ @Override
+ public <T> StateRouter<T> getRouter(Class<T> interfaceClass, URL url) {
+ return new XdsRouter<>(url);
+ }
+}
diff --git
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsScopeModelInitializer.java
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsScopeModelInitializer.java
new file mode 100644
index 0000000000..e94d4be4a9
--- /dev/null
+++
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/XdsScopeModelInitializer.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds;
+
+import org.apache.dubbo.common.beans.factory.ScopeBeanFactory;
+import org.apache.dubbo.rpc.model.ApplicationModel;
+import org.apache.dubbo.rpc.model.FrameworkModel;
+import org.apache.dubbo.rpc.model.ModuleModel;
+import org.apache.dubbo.rpc.model.ScopeModelInitializer;
+
+public class XdsScopeModelInitializer implements ScopeModelInitializer {
+
+ @Override
+ public void initializeFrameworkModel(FrameworkModel frameworkModel) {
+
+ }
+
+ @Override
+ public void initializeApplicationModel(ApplicationModel applicationModel) {
+ ScopeBeanFactory beanFactory = applicationModel.getBeanFactory();
+ beanFactory.registerBean(RdsRouteRuleManager.class);
+ beanFactory.registerBean(EdsEndpointManager.class);
+ }
+
+ @Override
+ public void initializeModuleModel(ModuleModel moduleModel) {
+ }
+}
diff --git
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/ClusterWeight.java
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/ClusterWeight.java
new file mode 100644
index 0000000000..a48857cea4
--- /dev/null
+++
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/ClusterWeight.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds.rule;
+
+public class ClusterWeight {
+
+ private final String name;
+
+ private final int weight;
+
+ public ClusterWeight(String name, int weight) {
+ this.name = name;
+ this.weight = weight;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getWeight() {
+ return weight;
+ }
+
+}
diff --git
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/DestinationSubset.java
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/DestinationSubset.java
new file mode 100644
index 0000000000..9644a687b0
--- /dev/null
+++
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/DestinationSubset.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds.rule;
+
+import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint;
+import org.apache.dubbo.rpc.Invoker;
+import org.apache.dubbo.rpc.cluster.router.state.BitList;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class DestinationSubset<T> {
+
+ public DestinationSubset(String clusterName) {
+ this.clusterName = clusterName;
+ }
+
+ private final String clusterName;
+
+ private Set<Endpoint> endpoints = new HashSet<>();
+
+ private BitList<Invoker<T>> invokers;
+
+ public String getClusterName() {
+ return clusterName;
+ }
+
+ public Set<Endpoint> getEndpoints() {
+ return endpoints;
+ }
+
+ public void setEndpoints(Set<Endpoint> endpoints) {
+ this.endpoints = endpoints;
+
+ }
+
+ public BitList<Invoker<T>> getInvokers() {
+ return invokers;
+ }
+
+ public void setInvokers(BitList<Invoker<T>> invokers) {
+ this.invokers = invokers;
+ }
+}
diff --git
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HTTPRouteDestination.java
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HTTPRouteDestination.java
new file mode 100644
index 0000000000..471e2a215a
--- /dev/null
+++
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HTTPRouteDestination.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds.rule;
+
+import java.util.List;
+
+public class HTTPRouteDestination {
+
+ private String cluster;
+
+ private List<ClusterWeight> weightedClusters;
+
+ public String getCluster() {
+ return cluster;
+ }
+
+ public void setCluster(String cluster) {
+ this.cluster = cluster;
+ }
+
+ public List<ClusterWeight> getWeightedClusters() {
+ return weightedClusters;
+ }
+
+ public void setWeightedClusters(List<ClusterWeight> weightedClusters) {
+ this.weightedClusters = weightedClusters;
+ }
+
+}
diff --git
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcher.java
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcher.java
new file mode 100644
index 0000000000..68bc38a9d5
--- /dev/null
+++
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcher.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds.rule;
+
+public class HeaderMatcher {
+
+
+ public String name;
+
+ public String exactValue;
+
+ private String regex;
+
+ public LongRangeMatch range;
+
+ public Boolean present;
+
+ public String prefix;
+
+ public String suffix;
+
+ public boolean inverted;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getExactValue() {
+ return exactValue;
+ }
+
+ public void setExactValue(String exactValue) {
+ this.exactValue = exactValue;
+ }
+
+ public String getRegex() {
+ return regex;
+ }
+
+ public void setRegex(String regex) {
+ this.regex = regex;
+ }
+
+ public LongRangeMatch getRange() {
+ return range;
+ }
+
+ public void setRange(LongRangeMatch range) {
+ this.range = range;
+ }
+
+ public Boolean getPresent() {
+ return present;
+ }
+
+ public void setPresent(Boolean present) {
+ this.present = present;
+ }
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ public void setPrefix(String prefix) {
+ this.prefix = prefix;
+ }
+
+ public String getSuffix() {
+ return suffix;
+ }
+
+ public void setSuffix(String suffix) {
+ this.suffix = suffix;
+ }
+
+ public boolean isInverted() {
+ return inverted;
+ }
+
+ public void setInverted(boolean inverted) {
+ this.inverted = inverted;
+ }
+
+ public boolean match(String input) {
+ if (getPresent() != null) {
+ return (input == null) == getPresent().equals(isInverted());
+ }
+ if (input == null) {
+ return false;
+ }
+ if (getExactValue() != null) {
+ return getExactValue().equals(input) != isInverted();
+ } else if (getRegex() != null) {
+ return input.matches(getRegex()) != isInverted();
+ } else if (getRange() != null) {
+ return getRange().isMatch(input) != isInverted();
+ } else if (getPrefix() != null) {
+ return input.startsWith(getPrefix()) != isInverted();
+ } else if (getSuffix() != null) {
+ return input.endsWith(getSuffix()) != isInverted();
+ }
+ return false;
+ }
+
+}
diff --git
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HttpRequestMatch.java
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HttpRequestMatch.java
new file mode 100644
index 0000000000..aff10ba890
--- /dev/null
+++
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HttpRequestMatch.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds.rule;
+
+import java.util.List;
+
+public class HttpRequestMatch {
+
+ private final PathMatcher pathMatcher;
+
+ private final List<HeaderMatcher> headerMatcherList;
+
+ public HttpRequestMatch(PathMatcher pathMatcher, List<HeaderMatcher>
headerMatcherList) {
+ this.pathMatcher = pathMatcher;
+ this.headerMatcherList = headerMatcherList;
+ }
+
+ public PathMatcher getPathMatcher() {
+ return pathMatcher;
+ }
+
+ public List<HeaderMatcher> getHeaderMatcherList() {
+ return headerMatcherList;
+ }
+
+
+}
diff --git
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/LongRangeMatch.java
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/LongRangeMatch.java
new file mode 100644
index 0000000000..d52123f03b
--- /dev/null
+++
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/LongRangeMatch.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.dubbo.rpc.cluster.router.xds.rule;
+
+public class LongRangeMatch {
+ private long start;
+ private long end;
+
+ public long getStart() {
+ return start;
+ }
+
+ public void setStart(long start) {
+ this.start = start;
+ }
+
+ public long getEnd() {
+ return end;
+ }
+
+ public void setEnd(long end) {
+ this.end = end;
+ }
+
+
+ public boolean isMatch(String input) {
+ try {
+ long num = Long.parseLong(input);
+ return num >= getStart() && num <= getEnd();
+ } catch (NumberFormatException ignore) {
+ return false;
+ }
+ }
+}
diff --git
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcher.java
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcher.java
new file mode 100644
index 0000000000..00931fb0a5
--- /dev/null
+++
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcher.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds.rule;
+
+
+public class PathMatcher {
+
+ private String path;
+
+ private String prefix;
+
+ private String regex;
+
+ private boolean caseSensitive;
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ public void setPrefix(String prefix) {
+ this.prefix = prefix;
+ }
+
+ public String getRegex() {
+ return regex;
+ }
+
+ public void setRegex(String regex) {
+ this.regex = regex;
+ }
+
+ public boolean isCaseSensitive() {
+ return caseSensitive;
+ }
+
+ public void setCaseSensitive(boolean caseSensitive) {
+ this.caseSensitive = caseSensitive;
+ }
+
+ public boolean isMatch(String input) {
+ if (getPath() != null) {
+ return isCaseSensitive()
+ ? getPath().equals(input)
+ : getPath().equalsIgnoreCase(input);
+ } else if (getPrefix() != null) {
+ return isCaseSensitive()
+ ? input.startsWith(getPrefix())
+ : input.toLowerCase().startsWith(getPrefix());
+ }
+ return input.matches(getRegex());
+ }
+}
diff --git
a/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/XdsRouteRule.java
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/XdsRouteRule.java
new file mode 100644
index 0000000000..2ca613bdd1
--- /dev/null
+++
b/dubbo-xds/src/main/java/org/apache/dubbo/rpc/cluster/router/xds/rule/XdsRouteRule.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds.rule;
+
+
+
+public class XdsRouteRule {
+
+ private final HttpRequestMatch match;
+
+ private final HTTPRouteDestination route;
+
+ public XdsRouteRule(HttpRequestMatch match, HTTPRouteDestination route) {
+ this.match = match;
+ this.route = route;
+ }
+
+
+ public HttpRequestMatch getMatch() {
+ return match;
+ }
+
+ public HTTPRouteDestination getRoute() {
+ return route;
+ }
+
+}
diff --git
a/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory
b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory
new file mode 100644
index 0000000000..ca9b94ea84
--- /dev/null
+++
b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory
@@ -0,0 +1 @@
+xds=org.apache.dubbo.rpc.cluster.router.xds.XdsRouterFactory
\ No newline at end of file
diff --git
a/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.model.ScopeModelInitializer
b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.model.ScopeModelInitializer
new file mode 100644
index 0000000000..3005831c66
--- /dev/null
+++
b/dubbo-xds/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.model.ScopeModelInitializer
@@ -0,0 +1 @@
+xds-route=org.apache.dubbo.rpc.cluster.router.xds.XdsScopeModelInitializer
diff --git
a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManagerTest.java
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManagerTest.java
new file mode 100644
index 0000000000..7da4950c55
--- /dev/null
+++
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/EdsEndpointManagerTest.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds;
+
+import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class EdsEndpointManagerTest {
+
+
+ @BeforeEach
+ public void before() {
+ EdsEndpointManager.getEdsListeners().clear();
+ EdsEndpointManager.getEndpointListeners().clear();
+ EdsEndpointManager.getEndpointDataCache().clear();
+ }
+
+ @Test
+ public void subscribeEdsTest() {
+ EdsEndpointManager manager = new EdsEndpointManager();
+ String cluster = "testApp";
+ int subscribeNum = 3;
+ for (int i = 0; i < subscribeNum; i++) {
+ manager.subscribeEds(cluster, new EdsEndpointListener() {
+ @Override
+ public void onEndPointChange(String cluster, Set<Endpoint>
endpoints) {
+
+ }
+ });
+ }
+ assertNotNull(EdsEndpointManager.getEdsListeners().get(cluster));
+
assertEquals(EdsEndpointManager.getEndpointListeners().get(cluster).size(),
subscribeNum);
+ }
+
+
+ @Test
+ public void unsubscribeRdsTest() {
+ EdsEndpointManager manager = new EdsEndpointManager();
+ String domain = "testApp";
+ EdsEndpointListener listener = new EdsEndpointListener() {
+ @Override
+ public void onEndPointChange(String cluster, Set<Endpoint>
endpoints) {
+
+ }
+ };
+ manager.subscribeEds(domain, listener);
+ assertNotNull(EdsEndpointManager.getEdsListeners().get(domain));
+
assertEquals(EdsEndpointManager.getEndpointListeners().get(domain).size(), 1);
+
+ manager.unSubscribeEds(domain, listener);
+ assertNull(EdsEndpointManager.getEdsListeners().get(domain));
+ assertNull(EdsEndpointManager.getEndpointListeners().get(domain));
+ }
+
+
+ @Test
+ public void notifyRuleChangeTest() {
+
+ Map<String, Set<Endpoint>> cacheData = new HashMap<>();
+ String domain = "testApp";
+ Set<Endpoint> endpoints = new HashSet<>();
+ Endpoint endpoint = new Endpoint();
+ endpoints.add(endpoint);
+
+ EdsEndpointListener listener = new EdsEndpointListener() {
+ @Override
+ public void onEndPointChange(String cluster, Set<Endpoint>
endpoints) {
+ cacheData.put(cluster, endpoints);
+ }
+ };
+
+ EdsEndpointManager manager = new EdsEndpointManager();
+ manager.subscribeEds(domain, listener);
+ manager.notifyEndpointChange(domain, endpoints);
+ assertEquals(cacheData.get(domain), endpoints);
+
+ Map<String, Set<Endpoint>> cacheData2 = new HashMap<>();
+ EdsEndpointListener listener2 = new EdsEndpointListener() {
+ @Override
+ public void onEndPointChange(String cluster, Set<Endpoint>
endpoints) {
+ cacheData2.put(cluster, endpoints);
+ }
+ };
+ manager.subscribeEds(domain, listener2);
+ assertEquals(cacheData2.get(domain), endpoints);
+ // clear
+ manager.notifyEndpointChange(domain, new HashSet<>());
+ assertEquals(cacheData.get(domain).size(), 0);
+ }
+
+}
diff --git
a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManagerTest.java
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManagerTest.java
new file mode 100644
index 0000000000..44e2ad16e5
--- /dev/null
+++
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsRouteRuleManagerTest.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds;
+
+import org.apache.dubbo.rpc.cluster.router.xds.rule.HTTPRouteDestination;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.HttpRequestMatch;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class RdsRouteRuleManagerTest {
+
+ @BeforeEach
+ public void before() {
+ RdsRouteRuleManager.getRuleListeners().clear();
+ RdsRouteRuleManager.getRouteDataCache().clear();
+ RdsRouteRuleManager.getRdsListeners().clear();
+ }
+
+
+ @Test
+ public void subscribeRdsTest() {
+ RdsRouteRuleManager manager = new RdsRouteRuleManager();
+ String domain = "testApp";
+ int subscribeNum = 3;
+ for (int i = 0; i < subscribeNum; i++) {
+ manager.subscribeRds(domain, new XdsRouteRuleListener() {
+ @Override
+ public void onRuleChange(String appName, List<XdsRouteRule>
xdsRouteRules) {
+
+ }
+
+ @Override
+ public void clearRule(String appName) {
+
+ }
+ });
+ }
+ assertNotNull(RdsRouteRuleManager.getRdsListeners().get(domain));
+
assertEquals(RdsRouteRuleManager.getRuleListeners().get(domain).size(),
subscribeNum);
+ }
+
+
+ @Test
+ public void unsubscribeRdsTest() {
+ RdsRouteRuleManager manager = new RdsRouteRuleManager();
+ String domain = "testApp";
+ XdsRouteRuleListener listener = new XdsRouteRuleListener() {
+ @Override
+ public void onRuleChange(String appName, List<XdsRouteRule>
xdsRouteRules) {
+
+ }
+
+ @Override
+ public void clearRule(String appName) {
+
+ }
+ };
+ manager.subscribeRds(domain, listener);
+ assertNotNull(RdsRouteRuleManager.getRdsListeners().get(domain));
+
assertEquals(RdsRouteRuleManager.getRuleListeners().get(domain).size(), 1);
+
+ manager.unSubscribeRds(domain, listener);
+ assertNull(RdsRouteRuleManager.getRdsListeners().get(domain));
+ assertNull(RdsRouteRuleManager.getRuleListeners().get(domain));
+ }
+
+
+ @Test
+ public void notifyRuleChangeTest() {
+
+ Map<String, List<XdsRouteRule>> cacheData = new HashMap<>();
+ String domain = "testApp";
+ List<XdsRouteRule> xdsRouteRules = new ArrayList<>();
+ XdsRouteRule rule = new XdsRouteRule(new HttpRequestMatch(null, null),
+ new HTTPRouteDestination());
+ xdsRouteRules.add(rule);
+
+ XdsRouteRuleListener listener = new XdsRouteRuleListener() {
+ @Override
+ public void onRuleChange(String appName, List<XdsRouteRule>
xdsRouteRules) {
+ cacheData.put(appName, xdsRouteRules);
+ }
+
+ @Override
+ public void clearRule(String appName) {
+ cacheData.remove(appName);
+ }
+ };
+
+ RdsRouteRuleManager manager = new RdsRouteRuleManager();
+ manager.subscribeRds(domain, listener);
+ manager.notifyRuleChange(domain, xdsRouteRules);
+ assertEquals(cacheData.get(domain), xdsRouteRules);
+
+ Map<String, List<XdsRouteRule>> cacheData2 = new HashMap<>();
+ XdsRouteRuleListener listener2 = new XdsRouteRuleListener() {
+ @Override
+ public void onRuleChange(String appName, List<XdsRouteRule>
xdsRouteRules) {
+ cacheData2.put(appName, xdsRouteRules);
+ }
+
+ @Override
+ public void clearRule(String appName) {
+ cacheData2.remove(appName);
+ }
+ };
+ manager.subscribeRds(domain, listener2);
+ assertEquals(cacheData2.get(domain), xdsRouteRules);
+ // clear
+ manager.notifyRuleChange(domain, new ArrayList<>());
+ assertNull(cacheData.get(domain));
+ }
+
+}
diff --git
a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListenerTest.java
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListenerTest.java
new file mode 100644
index 0000000000..2899e6f3b8
--- /dev/null
+++
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/RdsVirtualHostListenerTest.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds;
+
+import org.apache.dubbo.rpc.cluster.router.xds.rule.ClusterWeight;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.XdsRouteRule;
+
+import com.google.protobuf.BoolValue;
+import com.google.protobuf.UInt32Value;
+import io.envoyproxy.envoy.config.route.v3.HeaderMatcher;
+import io.envoyproxy.envoy.config.route.v3.Route;
+import io.envoyproxy.envoy.config.route.v3.RouteAction;
+import io.envoyproxy.envoy.config.route.v3.RouteMatch;
+import io.envoyproxy.envoy.config.route.v3.VirtualHost;
+import io.envoyproxy.envoy.config.route.v3.WeightedCluster;
+import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher;
+import io.envoyproxy.envoy.type.v3.Int64Range;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class RdsVirtualHostListenerTest {
+
+ private final String domain = "testApp";
+
+ private final Map<String, List<XdsRouteRule>> dataCache = new HashMap<>();
+
+ private final XdsRouteRuleListener listener = new XdsRouteRuleListener() {
+ @Override
+ public void onRuleChange(String appName, List<XdsRouteRule>
xdsRouteRules) {
+ dataCache.put(appName, xdsRouteRules);
+ }
+
+ @Override
+ public void clearRule(String appName) {
+ dataCache.remove(appName);
+ }
+ };
+
+ private final RdsRouteRuleManager manager = new RdsRouteRuleManager();
+
+ private final RdsVirtualHostListener rdsVirtualHostListener = new
RdsVirtualHostListener("testApp", manager);
+
+ @BeforeEach
+ public void init() {
+ dataCache.clear();
+ manager.subscribeRds(domain, listener);
+ }
+
+
+ @Test
+ public void parsePathPathMatcherTest() {
+ String path = "/test/name";
+ VirtualHost virtualHost = VirtualHost.newBuilder()
+ .addDomains(domain)
+ .addRoutes(Route.newBuilder().setName("route-test")
+
.setMatch(RouteMatch.newBuilder().setPath(path).setCaseSensitive(BoolValue.newBuilder().setValue(true).build()).build())
+
.setRoute(RouteAction.newBuilder().setCluster("cluster-test").build())
+ .build()
+ ).build();
+ rdsVirtualHostListener.parseVirtualHost(virtualHost);
+ List<XdsRouteRule> rules = dataCache.get(domain);
+ assertNotNull(rules);
+ assertEquals(rules.get(0).getMatch().getPathMatcher().getPath(), path);
+ assertTrue(rules.get(0).getMatch().getPathMatcher().isCaseSensitive());
+ }
+
+ @Test
+ public void parsePrefixPathMatcherTest() {
+ String prefix = "/test";
+ VirtualHost virtualHost = VirtualHost.newBuilder()
+ .addDomains(domain)
+ .addRoutes(Route.newBuilder().setName("route-test")
+ .setMatch(RouteMatch.newBuilder().setPrefix(prefix).build())
+
.setRoute(RouteAction.newBuilder().setCluster("cluster-test").build())
+ .build()
+ )
+ .build();
+ rdsVirtualHostListener.parseVirtualHost(virtualHost);
+ List<XdsRouteRule> rules = dataCache.get(domain);
+ assertNotNull(rules);
+ assertEquals(rules.get(0).getMatch().getPathMatcher().getPrefix(),
prefix);
+ }
+
+ @Test
+ public void parseRegexPathMatcherTest() {
+ String regex = "/test/.*";
+ VirtualHost virtualHost = VirtualHost.newBuilder()
+ .addDomains(domain)
+ .addRoutes(Route.newBuilder().setName("route-test")
+ .setMatch(RouteMatch.newBuilder().setSafeRegex(
+ RegexMatcher.newBuilder().setRegex(regex).build()
+ ).build())
+
.setRoute(RouteAction.newBuilder().setCluster("cluster-test").build())
+ .build()
+ )
+ .build();
+ rdsVirtualHostListener.parseVirtualHost(virtualHost);
+ List<XdsRouteRule> rules = dataCache.get(domain);
+ assertNotNull(rules);
+ assertEquals(rules.get(0).getMatch().getPathMatcher().getRegex(),
regex);
+ }
+
+ @Test
+ public void parseHeadMatcherTest() {
+ VirtualHost virtualHost = VirtualHost.newBuilder()
+ .addDomains(domain)
+ .addRoutes(Route.newBuilder().setName("route-test")
+ .setMatch(RouteMatch.newBuilder()
+ .addHeaders(HeaderMatcher.newBuilder()
+ .setName("head-exactValue")
+ .setExactMatch("exactValue")
+ .setInvertMatch(true)
+ .build())
+ .addHeaders(HeaderMatcher.newBuilder()
+ .setName("head-regex")
+
.setSafeRegexMatch(RegexMatcher.newBuilder().setRegex("regex").build())
+ .build())
+ .addHeaders(HeaderMatcher.newBuilder()
+ .setName("head-range")
+
.setRangeMatch(Int64Range.newBuilder().setStart(1).setEnd(100).build())
+ .build())
+ .addHeaders(HeaderMatcher.newBuilder()
+ .setName("head-present")
+ .setPresentMatch(true)
+ .build())
+ .addHeaders(HeaderMatcher.newBuilder()
+ .setName("head-prefix")
+ .setPrefixMatch("prefix")
+ .build())
+ .addHeaders(HeaderMatcher.newBuilder()
+ .setName("head-suffix")
+ .setSuffixMatch("suffix")
+ .build())
+ .build()
+ )
+
.setRoute(RouteAction.newBuilder().setCluster("cluster-test").build())
+ .build()
+ )
+ .build();
+ rdsVirtualHostListener.parseVirtualHost(virtualHost);
+ List<XdsRouteRule> rules = dataCache.get(domain);
+ assertNotNull(rules);
+ List<org.apache.dubbo.rpc.cluster.router.xds.rule.HeaderMatcher>
headerMatcherList = rules.get(0).getMatch().getHeaderMatcherList();
+ for (org.apache.dubbo.rpc.cluster.router.xds.rule.HeaderMatcher
matcher : headerMatcherList) {
+ if (matcher.getName().equals("head-exactValue")) {
+ assertEquals(matcher.getExactValue(), "exactValue");
+ } else if (matcher.getName().equals("head-regex")) {
+ assertEquals(matcher.getRegex(), "regex");
+ } else if (matcher.getName().equals("head-range")) {
+ assertEquals(matcher.getRange().getStart(), 1);
+ assertEquals(matcher.getRange().getEnd(), 100);
+ } else if (matcher.getName().equals("head-present")) {
+ assertTrue(matcher.getPresent());
+ } else if (matcher.getName().equals("head-prefix")) {
+ assertEquals(matcher.getPrefix(), "prefix");
+ } else if (matcher.getName().equals("head-suffix")) {
+ assertEquals(matcher.getSuffix(), "suffix");
+ }
+ }
+ }
+
+ @Test
+ public void parseRouteClusterTest() {
+ String cluster = "cluster-test";
+ VirtualHost virtualHost = VirtualHost.newBuilder()
+ .addDomains(domain)
+ .addRoutes(Route.newBuilder().setName("route-test")
+ .setMatch(RouteMatch.newBuilder().setPrefix("/test").build())
+ .setRoute(RouteAction.newBuilder().setCluster(cluster).build())
+ .build()
+ )
+ .build();
+ rdsVirtualHostListener.parseVirtualHost(virtualHost);
+ List<XdsRouteRule> rules = dataCache.get(domain);
+ assertNotNull(rules);
+ assertEquals(rules.get(0).getRoute().getCluster(), cluster);
+ }
+
+ @Test
+ public void parseRouteWeightClusterTest() {
+ VirtualHost virtualHost = VirtualHost.newBuilder()
+ .addDomains(domain)
+ .addRoutes(Route.newBuilder().setName("route-test")
+ .setMatch(RouteMatch.newBuilder().setPrefix("/test").build())
+ .setRoute(RouteAction.newBuilder().setWeightedClusters(
+ WeightedCluster.newBuilder()
+
.addClusters(WeightedCluster.ClusterWeight.newBuilder().setName("cluster-test1")
+
.setWeight(UInt32Value.newBuilder().setValue(40).build()).build())
+
.addClusters(WeightedCluster.ClusterWeight.newBuilder().setName("cluster-test2")
+
.setWeight(UInt32Value.newBuilder().setValue(60).build()).build())
+ .build()
+ ).build())
+ .build())
+ .build();
+ rdsVirtualHostListener.parseVirtualHost(virtualHost);
+ List<XdsRouteRule> rules = dataCache.get(domain);
+ assertNotNull(rules);
+ List<ClusterWeight> weightedClusters =
rules.get(0).getRoute().getWeightedClusters();
+ assertEquals(weightedClusters.size(), 2);
+ for (ClusterWeight weightedCluster : weightedClusters) {
+ if (weightedCluster.getName().equals("cluster-test1")) {
+ assertEquals(weightedCluster.getWeight(), 40);
+ } else if (weightedCluster.getName().equals("cluster-test2")) {
+ assertEquals(weightedCluster.getWeight(), 60);
+ }
+ }
+ }
+
+}
diff --git
a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteTest.java
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteTest.java
new file mode 100644
index 0000000000..e48ae10ace
--- /dev/null
+++
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/XdsRouteTest.java
@@ -0,0 +1,363 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.utils.Holder;
+import org.apache.dubbo.common.utils.StringUtils;
+import org.apache.dubbo.registry.xds.util.protocol.message.Endpoint;
+import org.apache.dubbo.rpc.Invocation;
+import org.apache.dubbo.rpc.Invoker;
+import org.apache.dubbo.rpc.cluster.router.mesh.util.TracingContextProvider;
+import org.apache.dubbo.rpc.cluster.router.state.BitList;
+import org.apache.dubbo.rpc.cluster.router.xds.rule.DestinationSubset;
+
+import com.google.protobuf.UInt32Value;
+import io.envoyproxy.envoy.config.route.v3.HeaderMatcher;
+import io.envoyproxy.envoy.config.route.v3.Route;
+import io.envoyproxy.envoy.config.route.v3.RouteAction;
+import io.envoyproxy.envoy.config.route.v3.RouteMatch;
+import io.envoyproxy.envoy.config.route.v3.VirtualHost;
+import io.envoyproxy.envoy.config.route.v3.WeightedCluster;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class XdsRouteTest {
+
+ private EdsEndpointManager edsEndpointManager;
+
+ private RdsRouteRuleManager rdsRouteRuleManager;
+ private Set<TracingContextProvider> tracingContextProviders;
+ private URL url;
+
+ @BeforeEach
+ public void setup() {
+ edsEndpointManager = Mockito.spy(EdsEndpointManager.class);
+ rdsRouteRuleManager = Mockito.spy(RdsRouteRuleManager.class);
+ tracingContextProviders = new HashSet<>();
+
+ url = URL.valueOf("test://localhost/DemoInterface");
+ }
+
+ private Invoker<Object> createInvoker(String app) {
+ URL url = URL.valueOf("dubbo://localhost/DemoInterface?" +
(StringUtils.isEmpty(app) ? "" : "remote.application=" + app));
+ Invoker invoker = Mockito.mock(Invoker.class);
+ when(invoker.getUrl()).thenReturn(url);
+ return invoker;
+ }
+
+ private Invoker<Object> createInvoker(String app, String address) {
+ URL url = URL.valueOf("dubbo://" + address + "/DemoInterface?" +
(StringUtils.isEmpty(app) ? "" : "remote.application=" + app));
+ Invoker invoker = Mockito.mock(Invoker.class);
+ when(invoker.getUrl()).thenReturn(url);
+ return invoker;
+ }
+
+ @Test
+ public void testNotifyInvoker() {
+ XdsRouter<Object> xdsRouter = new XdsRouter<>(url,
rdsRouteRuleManager, edsEndpointManager);
+ xdsRouter.notify(null);
+ assertEquals(0, xdsRouter.getSubscribeApplications().size());
+
+ BitList<Invoker<Object>> invokers = new
BitList<>(Arrays.asList(createInvoker(""), createInvoker("app1")));
+
+ xdsRouter.notify(invokers);
+
+ assertEquals(1, xdsRouter.getSubscribeApplications().size());
+ assertTrue(xdsRouter.getSubscribeApplications().contains("app1"));
+ assertEquals(invokers, xdsRouter.getInvokerList());
+
+ verify(rdsRouteRuleManager, times(1)).subscribeRds("app1", xdsRouter);
+
+ invokers = new BitList<>(Arrays.asList(createInvoker("app2")));
+ xdsRouter.notify(invokers);
+ verify(rdsRouteRuleManager, times(1)).subscribeRds("app2", xdsRouter);
+ verify(rdsRouteRuleManager, times(1)).unSubscribeRds("app1",
xdsRouter);
+ assertEquals(invokers, xdsRouter.getInvokerList());
+
+ xdsRouter.stop();
+ verify(rdsRouteRuleManager, times(1)).unSubscribeRds("app2",
xdsRouter);
+ }
+
+ @Test
+ public void testRuleChange() {
+ XdsRouter<Object> xdsRouter = new XdsRouter<>(url,
rdsRouteRuleManager, edsEndpointManager);
+ String appName = "app1";
+ String cluster1 = "cluster-test1";
+ String cluster2 = "cluster-test2";
+ BitList<Invoker<Object>> invokers = new
BitList<>(Arrays.asList(createInvoker(appName)));
+ xdsRouter.notify(invokers);
+ String path = "/DemoInterface/call";
+ VirtualHost virtualHost = VirtualHost.newBuilder()
+ .addDomains(appName)
+ .addRoutes(Route.newBuilder().setName("route-test")
+ .setMatch(RouteMatch.newBuilder().setPath(path).build())
+
.setRoute(RouteAction.newBuilder().setCluster(cluster1).build())
+ .build()
+ ).build();
+ RdsVirtualHostListener hostListener = new
RdsVirtualHostListener(appName, rdsRouteRuleManager);
+ hostListener.parseVirtualHost(virtualHost);
+ assertEquals(xdsRouter.getXdsRouteRuleMap().get(appName).size(), 1);
+ verify(edsEndpointManager, times(1)).subscribeEds(cluster1, xdsRouter);
+
+ VirtualHost virtualHost2 = VirtualHost.newBuilder()
+ .addDomains(appName)
+ .addRoutes(Route.newBuilder().setName("route-test")
+ .setMatch(RouteMatch.newBuilder().setPath(path).build())
+
.setRoute(RouteAction.newBuilder().setCluster("cluster-test2").build())
+ .build()
+ ).build();
+ hostListener.parseVirtualHost(virtualHost2);
+ assertEquals(xdsRouter.getXdsRouteRuleMap().get(appName).size(), 1);
+ verify(edsEndpointManager, times(1)).subscribeEds(cluster2, xdsRouter);
+ verify(edsEndpointManager, times(1)).unSubscribeEds(cluster1,
xdsRouter);
+ }
+
+
+ @Test
+ public void testEndpointChange() {
+ XdsRouter<Object> xdsRouter = new XdsRouter<>(url,
rdsRouteRuleManager, edsEndpointManager);
+ String appName = "app1";
+ String cluster1 = "cluster-test1";
+ BitList<Invoker<Object>> invokers = new
BitList<>(Arrays.asList(createInvoker(appName, "1.1.1.1:20880")
+ , createInvoker(appName, "2.2.2.2:20880")));
+ xdsRouter.notify(invokers);
+ String path = "/DemoInterface/call";
+ VirtualHost virtualHost = VirtualHost.newBuilder()
+ .addDomains(appName)
+ .addRoutes(Route.newBuilder().setName("route-test")
+ .setMatch(RouteMatch.newBuilder().setPath(path).build())
+
.setRoute(RouteAction.newBuilder().setCluster(cluster1).build())
+ .build()
+ ).build();
+ RdsVirtualHostListener hostListener = new
RdsVirtualHostListener(appName, rdsRouteRuleManager);
+ hostListener.parseVirtualHost(virtualHost);
+ assertEquals(xdsRouter.getXdsRouteRuleMap().get(appName).size(), 1);
+ verify(edsEndpointManager, times(1)).subscribeEds(cluster1, xdsRouter);
+
+ Set<Endpoint> endpoints = new HashSet<>();
+ Endpoint endpoint1 = new Endpoint();
+ endpoint1.setAddress("1.1.1.1");
+ endpoint1.setPortValue(20880);
+ Endpoint endpoint2 = new Endpoint();
+ endpoint2.setAddress("2.2.2.2");
+ endpoint2.setPortValue(20880);
+ endpoints.add(endpoint1);
+ endpoints.add(endpoint2);
+ edsEndpointManager.notifyEndpointChange(cluster1, endpoints);
+
+ DestinationSubset<Object> objectDestinationSubset =
xdsRouter.getDestinationSubsetMap().get(cluster1);
+ assertEquals(invokers, objectDestinationSubset.getInvokers());
+ }
+
+ @Test
+ public void testRouteNotMatch() {
+ XdsRouter<Object> xdsRouter = new XdsRouter<>(url,
rdsRouteRuleManager, edsEndpointManager);
+ String appName = "app1";
+ BitList<Invoker<Object>> invokers = new
BitList<>(Arrays.asList(createInvoker(appName, "1.1.1.1:20880")
+ , createInvoker(appName, "2.2.2.2:20880")));
+ assertEquals(invokers, xdsRouter.route(invokers.clone(), null, null,
false, null));
+ Holder<String> message = new Holder<>();
+ xdsRouter.doRoute(invokers.clone(), null, null, true, null, message);
+ assertEquals("Directly Return. Reason: xds route rule is empty.",
message.get());
+ }
+
+ @Test
+ public void testRoutePathMatch() {
+ XdsRouter<Object> xdsRouter = new XdsRouter<>(url,
rdsRouteRuleManager, edsEndpointManager);
+ String appName = "app1";
+ String cluster1 = "cluster-test1";
+ Invoker<Object> invoker1 = createInvoker(appName, "1.1.1.1:20880");
+ BitList<Invoker<Object>> invokers = new
BitList<>(Arrays.asList(invoker1
+ , createInvoker(appName, "2.2.2.2:20880")));
+ xdsRouter.notify(invokers);
+ String path = "/DemoInterface/call";
+ VirtualHost virtualHost = VirtualHost.newBuilder()
+ .addDomains(appName)
+ .addRoutes(Route.newBuilder().setName("route-test")
+ .setMatch(RouteMatch.newBuilder().setPath(path).build())
+
.setRoute(RouteAction.newBuilder().setCluster(cluster1).build())
+ .build()
+ ).build();
+ RdsVirtualHostListener hostListener = new
RdsVirtualHostListener(appName, rdsRouteRuleManager);
+ hostListener.parseVirtualHost(virtualHost);
+ Invocation invocation = Mockito.mock(Invocation.class);
+ Invoker invoker = Mockito.mock(Invoker.class);
+ URL url1 = Mockito.mock(URL.class);
+ when(invoker.getUrl()).thenReturn(url1);
+ when(url1.getPath()).thenReturn("DemoInterface");
+ when(invocation.getInvoker()).thenReturn(invoker);
+ when(invocation.getMethodName()).thenReturn("call");
+
+ Set<Endpoint> endpoints = new HashSet<>();
+ Endpoint endpoint1 = new Endpoint();
+ endpoint1.setAddress("1.1.1.1");
+ endpoint1.setPortValue(20880);
+ endpoints.add(endpoint1);
+ edsEndpointManager.notifyEndpointChange(cluster1, endpoints);
+ BitList<Invoker<Object>> routes = xdsRouter.route(invokers.clone(),
null, invocation, false, null);
+ assertEquals(1, routes.size());
+ assertEquals(invoker1, routes.get(0));
+
+ }
+
+
+ @Test
+ public void testRouteHeadMatch() {
+ XdsRouter<Object> xdsRouter = new XdsRouter<>(url,
rdsRouteRuleManager, edsEndpointManager);
+ String appName = "app1";
+ String cluster1 = "cluster-test1";
+ Invoker<Object> invoker1 = createInvoker(appName, "1.1.1.1:20880");
+ BitList<Invoker<Object>> invokers = new
BitList<>(Arrays.asList(invoker1
+ , createInvoker(appName, "2.2.2.2:20880")));
+ xdsRouter.notify(invokers);
+ VirtualHost virtualHost = VirtualHost.newBuilder()
+ .addDomains(appName)
+ .addRoutes(Route.newBuilder().setName("route-test")
+ .setMatch(RouteMatch.newBuilder().addHeaders(
+ HeaderMatcher.newBuilder()
+ .setName("userId")
+ .setExactMatch("123")
+ .build()
+ ).build()
+ )
+
.setRoute(RouteAction.newBuilder().setCluster(cluster1).build())
+ .build()
+ ).build();
+ RdsVirtualHostListener hostListener = new
RdsVirtualHostListener(appName, rdsRouteRuleManager);
+ hostListener.parseVirtualHost(virtualHost);
+ Invocation invocation = Mockito.mock(Invocation.class);
+ when(invocation.getAttachment("userId")).thenReturn("123");
+ Set<Endpoint> endpoints = new HashSet<>();
+ Endpoint endpoint1 = new Endpoint();
+ endpoint1.setAddress("1.1.1.1");
+ endpoint1.setPortValue(20880);
+ endpoints.add(endpoint1);
+ edsEndpointManager.notifyEndpointChange(cluster1, endpoints);
+ BitList<Invoker<Object>> routes = xdsRouter.route(invokers.clone(),
null, invocation, false, null);
+ assertEquals(1, routes.size());
+ assertEquals(invoker1, routes.get(0));
+ }
+
+
+ @Test
+ public void testRouteWeightCluster() {
+ XdsRouter<Object> xdsRouter = new XdsRouter<>(url,
rdsRouteRuleManager, edsEndpointManager);
+ String appName = "app1";
+ String cluster1 = "cluster-test1";
+ String cluster2 = "cluster-test2";
+ Invoker<Object> invoker1 = createInvoker(appName, "1.1.1.1:20880");
+ BitList<Invoker<Object>> invokers = new
BitList<>(Arrays.asList(invoker1
+ , createInvoker(appName, "2.2.2.2:20880")));
+ xdsRouter.notify(invokers);
+ VirtualHost virtualHost = VirtualHost.newBuilder()
+ .addDomains(appName)
+ .addRoutes(Route.newBuilder().setName("route-test")
+ .setMatch(RouteMatch.newBuilder().addHeaders(
+ HeaderMatcher.newBuilder()
+ .setName("userId")
+ .setExactMatch("123")
+ .build()
+ ).build()
+ )
+ .setRoute(RouteAction.newBuilder().setWeightedClusters(
+ WeightedCluster.newBuilder()
+
.addClusters(WeightedCluster.ClusterWeight.newBuilder().setName(cluster1)
+
.setWeight(UInt32Value.newBuilder().setValue(100).build()).build())
+
.addClusters(WeightedCluster.ClusterWeight.newBuilder().setName(cluster2)
+
.setWeight(UInt32Value.newBuilder().setValue(0).build()).build())
+ .build())
+ .build()
+ ).build()).build();
+ RdsVirtualHostListener hostListener = new
RdsVirtualHostListener(appName, rdsRouteRuleManager);
+ hostListener.parseVirtualHost(virtualHost);
+ Invocation invocation = Mockito.mock(Invocation.class);
+ when(invocation.getAttachment("userId")).thenReturn("123");
+ Set<Endpoint> endpoints = new HashSet<>();
+ Endpoint endpoint1 = new Endpoint();
+ endpoint1.setAddress("1.1.1.1");
+ endpoint1.setPortValue(20880);
+ endpoints.add(endpoint1);
+ edsEndpointManager.notifyEndpointChange(cluster1, endpoints);
+
+ endpoints = new HashSet<>();
+ Endpoint endpoint2 = new Endpoint();
+ endpoint2.setAddress("2.2.2.2");
+ endpoint2.setPortValue(20880);
+ endpoints.add(endpoint2);
+ edsEndpointManager.notifyEndpointChange(cluster2, endpoints);
+
+ for (int i = 0; i < 10; i++) {
+ BitList<Invoker<Object>> routes =
xdsRouter.route(invokers.clone(), null, invocation, false, null);
+ assertEquals(1, routes.size());
+ assertEquals(invoker1, routes.get(0));
+ }
+ }
+
+ @Test
+ public void testRouteMultiApp() {
+ XdsRouter<Object> xdsRouter = new XdsRouter<>(url,
rdsRouteRuleManager, edsEndpointManager);
+ String appName1 = "app1";
+ String appName2 = "app2";
+ String cluster1 = "cluster-test1";
+ Invoker<Object> invoker1 = createInvoker(appName2, "1.1.1.1:20880");
+ Invoker<Object> invoker2 = createInvoker(appName1, "2.2.2.2:20880");
+ BitList<Invoker<Object>> invokers = new
BitList<>(Arrays.asList(invoker1
+ , invoker2));
+ xdsRouter.notify(invokers);
+ assertEquals(xdsRouter.getSubscribeApplications().size(), 2);
+ String path = "/DemoInterface/call";
+ VirtualHost virtualHost = VirtualHost.newBuilder()
+ .addDomains(appName2)
+ .addRoutes(Route.newBuilder().setName("route-test")
+ .setMatch(RouteMatch.newBuilder().setPath(path).build())
+
.setRoute(RouteAction.newBuilder().setCluster(cluster1).build())
+ .build()
+ ).build();
+ RdsVirtualHostListener hostListener = new
RdsVirtualHostListener(appName2, rdsRouteRuleManager);
+ hostListener.parseVirtualHost(virtualHost);
+ Invocation invocation = Mockito.mock(Invocation.class);
+ Invoker invoker = Mockito.mock(Invoker.class);
+ URL url1 = Mockito.mock(URL.class);
+ when(invoker.getUrl()).thenReturn(url1);
+ when(url1.getPath()).thenReturn("DemoInterface");
+ when(invocation.getInvoker()).thenReturn(invoker);
+ when(invocation.getMethodName()).thenReturn("call");
+
+ Set<Endpoint> endpoints = new HashSet<>();
+ Endpoint endpoint1 = new Endpoint();
+ endpoint1.setAddress("1.1.1.1");
+ endpoint1.setPortValue(20880);
+ endpoints.add(endpoint1);
+ edsEndpointManager.notifyEndpointChange(cluster1, endpoints);
+ BitList<Invoker<Object>> routes = xdsRouter.route(invokers.clone(),
null, invocation, false, null);
+ assertEquals(1, routes.size());
+ assertEquals(invoker1, routes.get(0));
+ }
+
+}
diff --git
a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcherTest.java
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcherTest.java
new file mode 100644
index 0000000000..dc8e240482
--- /dev/null
+++
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/HeaderMatcherTest.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds.rule;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HeaderMatcherTest {
+
+ @Test
+ public void exactValueMatcherTest() {
+ HeaderMatcher headMatcher = new HeaderMatcher();
+ headMatcher.setName("testHead");
+ String value = "testValue";
+ headMatcher.setExactValue(value);
+ assertTrue(headMatcher.match(value));
+ }
+
+
+ @Test
+ public void regexMatcherTest() {
+ HeaderMatcher headMatcher = new HeaderMatcher();
+ headMatcher.setRegex("test.*");
+ String value = "testValue";
+ headMatcher.setExactValue(value);
+ assertTrue(headMatcher.match(value));
+ }
+
+ @Test
+ public void rangMatcherTest() {
+ HeaderMatcher headMatcher = new HeaderMatcher();
+ LongRangeMatch range = new LongRangeMatch();
+ range.setStart(100);
+ range.setEnd(500);
+ headMatcher.setRange(range);
+ assertTrue(headMatcher.match("300"));
+ }
+
+
+ @Test
+ public void presentMatcherTest() {
+ HeaderMatcher headMatcher = new HeaderMatcher();
+ headMatcher.setName("testHead");
+ headMatcher.setPresent(true);
+ assertTrue(headMatcher.match("value"));
+ headMatcher.setPresent(false);
+ assertTrue(headMatcher.match(null));
+ }
+
+ @Test
+ public void prefixMatcherTest() {
+ HeaderMatcher headMatcher = new HeaderMatcher();
+ headMatcher.setName("testHead");
+ headMatcher.setPrefix("test");
+ assertTrue(headMatcher.match("testValue"));
+ }
+
+
+ @Test
+ public void suffixMatcherTest() {
+ HeaderMatcher headMatcher = new HeaderMatcher();
+ headMatcher.setName("testHead");
+ headMatcher.setSuffix("Value");
+ assertTrue(headMatcher.match("testValue"));
+ }
+
+ @Test
+ public void invertedMatcherTest() {
+ HeaderMatcher headMatcher = new HeaderMatcher();
+ headMatcher.setName("testHead");
+ String value = "testValue";
+ headMatcher.setExactValue(value);
+ headMatcher.setInverted(true);
+ assertFalse(headMatcher.match("testValue"));
+ }
+}
diff --git
a/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcherTest.java
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcherTest.java
new file mode 100644
index 0000000000..51bb920a6c
--- /dev/null
+++
b/dubbo-xds/src/test/java/org/apache/dubbo/rpc/cluster/router/xds/rule/PathMatcherTest.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.cluster.router.xds.rule;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class PathMatcherTest {
+
+
+ @Test
+ public void pathMatcherTest() {
+ PathMatcher pathMatcher = new PathMatcher();
+ String path = "/testService/test";
+ pathMatcher.setPath(path);
+ assertTrue(pathMatcher.isMatch(path));
+ assertTrue(pathMatcher.isMatch(path.toUpperCase()));
+ pathMatcher.setCaseSensitive(true);
+ assertFalse(pathMatcher.isMatch(path.toUpperCase()));
+
+ }
+
+ @Test
+ public void prefixMatcherTest() {
+ PathMatcher pathMatcher = new PathMatcher();
+ String prefix = "/test";
+ String path = "/testService/test";
+ pathMatcher.setPrefix(prefix);
+ assertTrue(pathMatcher.isMatch(path));
+ assertTrue(pathMatcher.isMatch(path.toUpperCase()));
+ pathMatcher.setCaseSensitive(true);
+ assertFalse(pathMatcher.isMatch(path.toUpperCase()));
+ }
+
+ @Test
+ public void regexMatcherTest() {
+ PathMatcher pathMatcher = new PathMatcher();
+ String regex = "/testService/.*";
+ String path = "/testService/test";
+ pathMatcher.setRegex(regex);
+ assertTrue(pathMatcher.isMatch(path));
+ }
+
+}