diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/Constants.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/Constants.java index 0e59f2cacf4..a5a85590ef6 100644 --- a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/Constants.java +++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/Constants.java @@ -138,4 +138,12 @@ public interface Constants { String RULE_VERSION_V27 = "v2.7"; String RULE_VERSION_V30 = "v3.0"; + + String RULE_VERSION_V31 = "v3.1"; + + public static final String TRAFFIC_DISABLE_KEY = "trafficDisable"; + public static final String RATIO_KEY = "ratio"; + public static final int DefaultRouteRatio = 0; + public static final int DefaultRouteConditionSubSetWeight = 100; + public static final int DefaultRoutePriority = 0; } diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/MultiDestConditionRouter.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/MultiDestConditionRouter.java new file mode 100644 index 00000000000..23d9a338c21 --- /dev/null +++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/MultiDestConditionRouter.java @@ -0,0 +1,409 @@ +/* + * 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.condition; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.dubbo.common.utils.Holder; +import org.apache.dubbo.common.utils.NetUtils; +import org.apache.dubbo.common.utils.StringUtils; +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.condition.config.model.ConditionSubSet; +import org.apache.dubbo.rpc.cluster.router.condition.config.model.DestinationSet; +import org.apache.dubbo.rpc.cluster.router.condition.config.model.MultiDestCondition; +import org.apache.dubbo.rpc.cluster.router.condition.matcher.ConditionMatcher; +import org.apache.dubbo.rpc.cluster.router.condition.matcher.ConditionMatcherFactory; +import org.apache.dubbo.rpc.cluster.router.state.AbstractStateRouter; +import org.apache.dubbo.rpc.cluster.router.state.BitList; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.apache.dubbo.common.constants.LoggerCodeConstants.CLUSTER_CONDITIONAL_ROUTE_LIST_EMPTY; +import static org.apache.dubbo.common.constants.LoggerCodeConstants.CLUSTER_FAILED_EXEC_CONDITION_ROUTER; +import static org.apache.dubbo.rpc.cluster.Constants.DefaultRouteConditionSubSetWeight; +import static org.apache.dubbo.rpc.cluster.Constants.RULE_KEY; + +public class MultiDestConditionRouter extends AbstractStateRouter { + public static final String NAME = "multi_condition"; + + private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(AbstractStateRouter.class); + protected static final Pattern ROUTE_PATTERN = Pattern.compile("([&!=,]*)\\s*([^&!=,\\s]+)"); + private Map whenCondition; + private boolean trafficDisable; + private List thenCondition; + private int ratio; + private int priority; + private boolean force; + protected List matcherFactories; + private boolean enabled; + + public MultiDestConditionRouter(URL url, MultiDestCondition multiDestCondition, boolean enabled) { + super(url); + this.enabled = enabled; + matcherFactories = + moduleModel.getExtensionLoader(ConditionMatcherFactory.class).getActivateExtensions(); + this.covert(multiDestCondition, this); + this.init(multiDestCondition.getFrom(), multiDestCondition.getTo()); + } + + public void init(Map from, List> to) { + try { + if (from == null || to == null) { + throw new IllegalArgumentException("Illegal route rule!"); + } + String whenRule = from.get("match"); + Map when = + StringUtils.isBlank(whenRule) || "true".equals(whenRule) ? new HashMap<>() : parseRule(whenRule); + this.whenCondition = when; + + List thenConditions = new ArrayList<>(); + for (Map toMap : to) { + String thenRule = toMap.get("match"); + Map then = StringUtils.isBlank(thenRule) || "false".equals(thenRule) + ? new HashMap<>() + : parseRule(thenRule); + // NOTE: It should be determined on the business level whether the `When condition` can be empty or not. + + thenConditions.add(new ConditionSubSet( + then, + Integer.valueOf( + toMap.getOrDefault("weight", String.valueOf(DefaultRouteConditionSubSetWeight))))); + } + this.thenCondition = thenConditions; + } catch (ParseException e) { + throw new IllegalStateException(e.getMessage(), e); + } + } + + private Map parseRule(String rule) throws ParseException { + Map condition = new HashMap<>(); + if (StringUtils.isBlank(rule)) { + return condition; + } + // Key-Value pair, stores both match and mismatch conditions + ConditionMatcher matcherPair = null; + // Multiple values + Set values = null; + final Matcher matcher = ROUTE_PATTERN.matcher(rule); + while (matcher.find()) { // Try to match one by one + String separator = matcher.group(1); + String content = matcher.group(2); + // Start part of the condition expression. + if (StringUtils.isEmpty(separator)) { + matcherPair = this.getMatcher(content); + condition.put(content, matcherPair); + } + // The KV part of the condition expression + else if ("&".equals(separator)) { + if (condition.get(content) == null) { + matcherPair = this.getMatcher(content); + condition.put(content, matcherPair); + } else { + matcherPair = condition.get(content); + } + } + // The Value in the KV part. + else if ("=".equals(separator)) { + if (matcherPair == null) { + throw new ParseException( + "Illegal route rule \"" + rule + "\", The error char '" + separator + "' at index " + + matcher.start() + " before \"" + content + "\".", + matcher.start()); + } + + values = matcherPair.getMatches(); + values.add(content); + } + // The Value in the KV part. + else if ("!=".equals(separator)) { + if (matcherPair == null) { + throw new ParseException( + "Illegal route rule \"" + rule + "\", The error char '" + separator + "' at index " + + matcher.start() + " before \"" + content + "\".", + matcher.start()); + } + + values = matcherPair.getMismatches(); + values.add(content); + } + // The Value in the KV part, if Value have more than one items. + else if (",".equals(separator)) { // Should be separated by ',' + if (values == null || values.isEmpty()) { + throw new ParseException( + "Illegal route rule \"" + rule + "\", The error char '" + separator + "' at index " + + matcher.start() + " before \"" + content + "\".", + matcher.start()); + } + values.add(content); + } else { + throw new ParseException( + "Illegal route rule \"" + rule + "\", The error char '" + separator + "' at index " + + matcher.start() + " before \"" + content + "\".", + matcher.start()); + } + } + return condition; + } + + private ConditionMatcher getMatcher(String key) { + for (ConditionMatcherFactory factory : matcherFactories) { + if (factory.shouldMatch(key)) { + return factory.createMatcher(key, moduleModel); + } + } + return moduleModel + .getExtensionLoader(ConditionMatcherFactory.class) + .getExtension("param") + .createMatcher(key, moduleModel); + } + + @Override + protected BitList> doRoute( + BitList> invokers, + URL url, + Invocation invocation, + boolean needToPrintMessage, + Holder> routerSnapshotNodeHolder, + Holder messageHolder) + throws RpcException { + + if (!enabled) { + if (needToPrintMessage) { + messageHolder.set("Directly return. Reason: ConditionRouter disabled."); + } + return invokers; + } + + if (CollectionUtils.isEmpty(invokers)) { + if (needToPrintMessage) { + messageHolder.set("Directly return. Reason: Invokers from previous router is empty."); + } + return invokers; + } + + try { + if (!matchWhen(url, invocation)) { + if (needToPrintMessage) { + messageHolder.set("Directly return. Reason: WhenCondition not match."); + } + return invokers; + } + if (trafficDisable) { + if (needToPrintMessage) { + messageHolder.set("Directly return. Reason: TrafficDisableKey is true."); + } + return BitList.emptyList(); + } + if (thenCondition == null || thenCondition.size() == 0) { + logger.warn( + CLUSTER_CONDITIONAL_ROUTE_LIST_EMPTY, + "condition state router thenCondition is empty", + "", + "The current consumer in the service blocklist. consumer: " + NetUtils.getLocalHost() + + ", service: " + url.getServiceKey()); + if (needToPrintMessage) { + messageHolder.set("Empty return. Reason: ThenCondition is empty."); + } + return returnByForce(invokers, !this.isForce(), true); + } + + DestinationSet destinations = new DestinationSet(); + for (ConditionSubSet condition : thenCondition) { + BitList> res = invokers.clone(); + + for (Invoker invoker : invokers) { + if (!doMatch(invoker.getUrl(), url, null, condition.getCondition(), false)) { + res.remove(invoker); + } + } + if (!res.isEmpty()) { + destinations.addDestination( + condition.getSubSetWeight() == null + ? DefaultRouteConditionSubSetWeight + : condition.getSubSetWeight(), + res.clone()); + } + } + + if (!destinations.getDestinations().isEmpty()) { + BitList> res = destinations.randDestination(); + if (res.size() * 100 / invokers.size() > this.getRatio()) { + if (needToPrintMessage) { + messageHolder.set("Match return."); + } + return res; + } else { + if (needToPrintMessage) { + messageHolder.set("Empty return. Reason: Ratio not match."); + } + return returnByForce(invokers, !this.isForce(), true); + } + } else { + logger.warn( + CLUSTER_CONDITIONAL_ROUTE_LIST_EMPTY, + "execute condition state router result list is empty. and force=true", + "", + "The route result is empty and force execute. consumer: " + NetUtils.getLocalHost() + + ", service: " + url.getServiceKey() + ", router: " + + url.getParameterAndDecoded(RULE_KEY)); + if (needToPrintMessage) { + messageHolder.set("Empty return. Reason: Empty result from condition and condition is force."); + } + + return returnByForce(invokers, !this.isForce(), true); + } + + } catch (Throwable t) { + logger.error( + CLUSTER_FAILED_EXEC_CONDITION_ROUTER, + "execute condition state router exception", + "", + "Failed to execute condition router rule: " + getUrl() + ", invokers: " + invokers + ", cause: " + + t.getMessage(), + t); + } + + if (needToPrintMessage) { + messageHolder.set("Directly return. Reason: Error occurred ."); + } + return invokers.clone(); + } + + public BitList> returnByForce(BitList> invokers, boolean canContinue, boolean empty) { + if (canContinue && empty) { + return BitList.emptyList(); + } else if (canContinue && !empty) { + return invokers; + } else if (empty) { + return BitList.emptyList(); + } else { + return invokers.clone(); + } + } + + public void covert(MultiDestCondition multiDestCondition, MultiDestConditionRouter multiDestConditionRouter) { + if (multiDestCondition == null) { + throw new IllegalStateException("multiDestCondition is null"); + } + multiDestConditionRouter.setTrafficDisable(multiDestCondition.isTrafficDisable()); + multiDestConditionRouter.setRatio(multiDestCondition.getRatio()); + multiDestConditionRouter.setPriority(multiDestCondition.getPriority()); + multiDestConditionRouter.setForce(multiDestCondition.isForce()); + multiDestConditionRouter.setTrafficDisable(multiDestCondition.isTrafficDisable()); + } + + boolean matchWhen(URL url, Invocation invocation) { + if (CollectionUtils.isEmptyMap(whenCondition)) { + return true; + } + + return doMatch(url, null, invocation, whenCondition, true); + } + + private boolean doMatch( + URL url, + URL param, + Invocation invocation, + Map conditions, + boolean isWhenCondition) { + Map sample = url.toOriginalMap(); + for (Map.Entry entry : conditions.entrySet()) { + ConditionMatcher matchPair = entry.getValue(); + + if (!matchPair.isMatch(sample, param, invocation, isWhenCondition)) { + return false; + } + } + return true; + } + + public void setWhenCondition(Map whenCondition) { + this.whenCondition = whenCondition; + } + + public void setTrafficDisable(boolean trafficDisable) { + this.trafficDisable = trafficDisable; + } + + public void setThenCondition(List thenCondition) { + this.thenCondition = thenCondition; + } + + public void setRatio(int ratio) { + this.ratio = ratio; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + public void setForce(boolean force) { + this.force = force; + } + + public Map getWhenCondition() { + return whenCondition; + } + + public boolean isTrafficDisable() { + return trafficDisable; + } + + public int getRatio() { + return ratio; + } + + public int getPriority() { + return priority; + } + + public boolean isForce() { + return force; + } + + public List getThenCondition() { + return thenCondition; + } + + public List getMatcherFactories() { + return matcherFactories; + } + + public void setMatcherFactories(List matcherFactories) { + this.matcherFactories = matcherFactories; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/ListenableStateRouter.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/ListenableStateRouter.java index 575bd462822..01fe2c4a226 100644 --- a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/ListenableStateRouter.java +++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/ListenableStateRouter.java @@ -29,10 +29,13 @@ 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.AbstractRouterRule; import org.apache.dubbo.rpc.cluster.router.RouterSnapshotNode; import org.apache.dubbo.rpc.cluster.router.condition.ConditionStateRouter; +import org.apache.dubbo.rpc.cluster.router.condition.MultiDestConditionRouter; import org.apache.dubbo.rpc.cluster.router.condition.config.model.ConditionRouterRule; import org.apache.dubbo.rpc.cluster.router.condition.config.model.ConditionRuleParser; +import org.apache.dubbo.rpc.cluster.router.condition.config.model.MultiDestConditionRouterRule; 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.state.TailStateRouter; @@ -42,6 +45,7 @@ import java.util.stream.Collectors; import static org.apache.dubbo.common.constants.LoggerCodeConstants.CLUSTER_FAILED_RULE_PARSING; +import static org.apache.dubbo.rpc.cluster.Constants.RULE_VERSION_V31; /** * Abstract router which listens to dynamic configuration @@ -52,8 +56,11 @@ public abstract class ListenableStateRouter extends AbstractStateRouter im private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(ListenableStateRouter.class); - private volatile ConditionRouterRule routerRule; + private volatile AbstractRouterRule routerRule; private volatile List> conditionRouters = Collections.emptyList(); + + // for v3.1 + private volatile List> multiDestConditionRouters = Collections.emptyList(); private final String ruleKey; public ListenableStateRouter(URL url, String ruleKey) { @@ -73,6 +80,8 @@ public synchronized void process(ConfigChangedEvent event) { if (event.getChangeType().equals(ConfigChangeType.DELETED)) { routerRule = null; conditionRouters = Collections.emptyList(); + // for v3.1 + multiDestConditionRouters = Collections.emptyList(); } else { try { routerRule = ConditionRuleParser.parse(event.getContent()); @@ -99,7 +108,8 @@ public BitList> doRoute( Holder> nodeHolder, Holder messageHolder) throws RpcException { - if (CollectionUtils.isEmpty(invokers) || conditionRouters.size() == 0) { + if (CollectionUtils.isEmpty(invokers) + || (conditionRouters.size() == 0 && multiDestConditionRouters.size() == 0)) { if (needToPrintMessage) { messageHolder.set( "Directly return. Reason: Invokers from previous router is empty or conditionRouters is empty."); @@ -112,18 +122,45 @@ public BitList> doRoute( if (needToPrintMessage) { resultMessage = new StringBuilder(); } - for (AbstractStateRouter router : conditionRouters) { - invokers = router.route(invokers, url, invocation, needToPrintMessage, nodeHolder); - if (needToPrintMessage) { - resultMessage.append(messageHolder.get()); + + BitList> routeResult = invokers; + if (routerRule instanceof MultiDestConditionRouterRule + || routerRule.getVersion() != null && routerRule.getVersion().startsWith(RULE_VERSION_V31)) { + boolean trafficDisable = false; + for (MultiDestConditionRouter multiDestConditionRouter : multiDestConditionRouters) { + routeResult = multiDestConditionRouter.route(invokers, url, invocation, needToPrintMessage, nodeHolder); + if (invokers == routeResult) { + // not match or disable to continue next multiDestConditionRouter + continue; + } else if (routeResult.size() == 0 + && !multiDestConditionRouter.isTrafficDisable() + && !multiDestConditionRouter.isForce()) { + // empty but can continue to next multiDestConditionRouter + continue; + } else { + trafficDisable = multiDestConditionRouter.isTrafficDisable(); + break; + } + } + // if trafficDisable ignore root.force + if (routeResult.size() == 0 && !routerRule.isForce() && !trafficDisable) { + routeResult = invokers; + } + } else { + for (AbstractStateRouter router : conditionRouters) { + routeResult = router.route(routeResult, url, invocation, needToPrintMessage, nodeHolder); } } + if (needToPrintMessage) { + resultMessage.append(messageHolder.get()); + } + if (needToPrintMessage) { messageHolder.set(resultMessage.toString()); } - return invokers; + return routeResult; } @Override @@ -135,15 +172,31 @@ private boolean isRuleRuntime() { return routerRule != null && routerRule.isValid() && routerRule.isRuntime(); } - private void generateConditions(ConditionRouterRule rule) { - if (rule != null && rule.isValid()) { - this.conditionRouters = rule.getConditions().stream() - .map(condition -> - new ConditionStateRouter(getUrl(), condition, rule.isForce(), rule.isEnabled())) - .collect(Collectors.toList()); + private void generateConditions(AbstractRouterRule rule) { + if (rule == null || !rule.isValid()) { + return; + } + + if (rule instanceof ConditionRouterRule) { + this.conditionRouters = ((ConditionRouterRule) rule) + .getConditions().stream() + .map(condition -> + new ConditionStateRouter(getUrl(), condition, rule.isForce(), rule.isEnabled())) + .collect(Collectors.toList()); + for (ConditionStateRouter conditionRouter : this.conditionRouters) { conditionRouter.setNextRouter(TailStateRouter.getInstance()); } + } else if (rule instanceof MultiDestConditionRouterRule) { + this.multiDestConditionRouters = ((MultiDestConditionRouterRule) rule) + .getConditions().stream() + .map(condition -> new MultiDestConditionRouter(getUrl(), condition, rule.isEnabled())) + .sorted((a, b) -> a.getPriority() - b.getPriority()) + .collect(Collectors.toList()); + + for (MultiDestConditionRouter conditionRouter : this.multiDestConditionRouters) { + conditionRouter.setNextRouter(TailStateRouter.getInstance()); + } } } diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/ConditionRouterRule.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/ConditionRouterRule.java index 7d9c936c3f8..08469da0855 100644 --- a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/ConditionRouterRule.java +++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/ConditionRouterRule.java @@ -28,7 +28,7 @@ public class ConditionRouterRule extends AbstractRouterRule { private List conditions; @SuppressWarnings("unchecked") - public static ConditionRouterRule parseFromMap(Map map) { + public static AbstractRouterRule parseFromMap(Map map) { ConditionRouterRule conditionRouterRule = new ConditionRouterRule(); conditionRouterRule.parseFromMap0(map); diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/ConditionRuleParser.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/ConditionRuleParser.java index 9aa80f908bb..37e9da19c60 100644 --- a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/ConditionRuleParser.java +++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/ConditionRuleParser.java @@ -16,7 +16,10 @@ */ package org.apache.dubbo.rpc.cluster.router.condition.config.model; +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.dubbo.rpc.cluster.router.AbstractRouterRule; import java.util.Map; @@ -24,6 +27,10 @@ import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; +import static org.apache.dubbo.common.constants.LoggerCodeConstants.CLUSTER_FAILED_RULE_PARSING; +import static org.apache.dubbo.rpc.cluster.Constants.CONFIG_VERSION_KEY; +import static org.apache.dubbo.rpc.cluster.Constants.RULE_VERSION_V31; + /** * %YAML1.2 * @@ -40,14 +47,36 @@ */ public class ConditionRuleParser { - public static ConditionRouterRule parse(String rawRule) { + private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(ConditionRuleParser.class); + + public static AbstractRouterRule parse(String rawRule) { + AbstractRouterRule rule; Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); Map map = yaml.load(rawRule); - ConditionRouterRule rule = ConditionRouterRule.parseFromMap(map); - rule.setRawRule(rawRule); - if (CollectionUtils.isEmpty(rule.getConditions())) { + String confVersion = (String) map.get(CONFIG_VERSION_KEY); + + if (confVersion != null && confVersion.toLowerCase().startsWith(RULE_VERSION_V31)) { + rule = MultiDestConditionRouterRule.parseFromMap(map); + if (CollectionUtils.isEmpty(((MultiDestConditionRouterRule) rule).getConditions())) { + rule.setValid(false); + } + } else if (confVersion != null && confVersion.compareToIgnoreCase(RULE_VERSION_V31) > 0) { + logger.warn( + CLUSTER_FAILED_RULE_PARSING, + "Invalid condition config version number.", + "", + "Ignore this configuration. Only " + RULE_VERSION_V31 + " and below are supported in this release"); +// rule = ConditionRouterRule.parseFromMap(map); + rule = new ConditionRouterRule(); rule.setValid(false); + } else { + // for under v3.1 + rule = ConditionRouterRule.parseFromMap(map); + if (CollectionUtils.isEmpty(((ConditionRouterRule) rule).getConditions())) { + rule.setValid(false); + } } + rule.setRawRule(rawRule); return rule; } diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/ConditionSubSet.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/ConditionSubSet.java new file mode 100644 index 00000000000..3f1577222d2 --- /dev/null +++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/ConditionSubSet.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.condition.config.model; + +import org.apache.dubbo.rpc.cluster.router.condition.matcher.ConditionMatcher; + +import java.util.HashMap; +import java.util.Map; + +import static org.apache.dubbo.rpc.cluster.Constants.DefaultRouteConditionSubSetWeight; + +public class ConditionSubSet { + private Map condition = new HashMap<>(); + private Integer subSetWeight; + + public ConditionSubSet() {} + + public ConditionSubSet(Map condition, Integer subSetWeight) { + this.condition = condition; + this.subSetWeight = subSetWeight; + if (subSetWeight <= 0) { + this.subSetWeight = DefaultRouteConditionSubSetWeight; + } + } + + public Map getCondition() { + return condition; + } + + public void setCondition(Map condition) { + this.condition = condition; + } + + public Integer getSubSetWeight() { + return subSetWeight; + } + + public void setSubSetWeight(int subSetWeight) { + this.subSetWeight = subSetWeight; + } + + @Override + public String toString() { + return "ConditionSubSet{" + "cond=" + condition + ", subSetWeight=" + subSetWeight + '}'; + } +} diff --git a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/java/org/apache/dubbo/demo/provider/TripleServiceImpl.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/Destination.java similarity index 53% rename from dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/java/org/apache/dubbo/demo/provider/TripleServiceImpl.java rename to dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/Destination.java index f1861454d1b..f5bb74c7e2c 100644 --- a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/java/org/apache/dubbo/demo/provider/TripleServiceImpl.java +++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/Destination.java @@ -14,14 +14,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.dubbo.demo.provider; +package org.apache.dubbo.rpc.cluster.router.condition.config.model; -import org.apache.dubbo.demo.TripleService; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.cluster.router.state.BitList; -public class TripleServiceImpl implements TripleService { +public class Destination { + private int weight; + private BitList> invokers; - @Override - public String hello() { - return "Triple!"; + Destination(int weight, BitList> invokers) { + this.weight = weight; + this.invokers = invokers; + } + + public int getWeight() { + return weight; + } + + public void setWeight(int weight) { + this.weight = weight; + } + + public BitList> getInvokers() { + return invokers; + } + + public void setInvokers(BitList> invokers) { + this.invokers = invokers; } } diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/DestinationSet.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/DestinationSet.java new file mode 100644 index 00000000000..42f7eef2a29 --- /dev/null +++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/DestinationSet.java @@ -0,0 +1,73 @@ +/* + * 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.condition.config.model; + +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.cluster.router.state.BitList; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +public class DestinationSet { + private final List> destinations; + private long weightSum; + private final ThreadLocalRandom random; + + public DestinationSet() { + this.destinations = new ArrayList<>(); + this.weightSum = 0; + this.random = ThreadLocalRandom.current(); + } + + public void addDestination(int weight, BitList> invokers) { + destinations.add(new Destination(weight, invokers)); + weightSum += weight; + } + + public BitList> randDestination() { + if (destinations.size() == 1) { + return destinations.get(0).getInvokers(); + } + + long sum = random.nextLong(weightSum); + for (Destination destination : destinations) { + sum -= destination.getWeight(); + if (sum <= 0) { + return destination.getInvokers(); + } + } + return BitList.emptyList(); + } + + public List> getDestinations() { + return destinations; + } + + public long getWeightSum() { + return weightSum; + } + + public void setWeightSum(long weightSum) { + this.weightSum = weightSum; + } + + public Random getRandom() { + return random; + } +} diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/MultiDestCondition.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/MultiDestCondition.java new file mode 100644 index 00000000000..56528399f4f --- /dev/null +++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/MultiDestCondition.java @@ -0,0 +1,85 @@ +/* + * 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.condition.config.model; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MultiDestCondition { + private int priority; + private Map from = new HashMap<>(); + private boolean trafficDisable; + private List> to = new ArrayList<>(); + private boolean force; + private int ratio; + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + public Map getFrom() { + return from; + } + + public void setFrom(Map from) { + this.from = from; + } + + public boolean isTrafficDisable() { + return trafficDisable; + } + + public void setTrafficDisable(boolean trafficDisable) { + this.trafficDisable = trafficDisable; + } + + public List> getTo() { + return to; + } + + public void setTo(List> to) { + this.to = to; + } + + public boolean isForce() { + return force; + } + + public void setForce(boolean force) { + this.force = force; + } + + public int getRatio() { + return ratio; + } + + public void setRatio(int ratio) { + this.ratio = ratio; + } + + @Override + public String toString() { + return "MultiDestCondition{" + "priority=" + priority + ", from=" + from + ", trafficDisable=" + trafficDisable + + ", to=" + to + ", force=" + force + ", ratio=" + ratio + '}'; + } +} diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/MultiDestConditionRouterRule.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/MultiDestConditionRouterRule.java new file mode 100644 index 00000000000..1a0160e0b61 --- /dev/null +++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/model/MultiDestConditionRouterRule.java @@ -0,0 +1,56 @@ +/* + * 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.condition.config.model; + +import org.apache.dubbo.common.utils.JsonUtils; +import org.apache.dubbo.rpc.cluster.router.AbstractRouterRule; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import static org.apache.dubbo.rpc.cluster.Constants.CONDITIONS_KEY; + +public class MultiDestConditionRouterRule extends AbstractRouterRule { + + private List conditions; + + public static AbstractRouterRule parseFromMap(Map map) { + + MultiDestConditionRouterRule multiDestConditionRouterRule = new MultiDestConditionRouterRule(); + multiDestConditionRouterRule.parseFromMap0(map); + List> conditions = (List>) map.get(CONDITIONS_KEY); + List multiDestConditions = new ArrayList<>(); + + for (Map condition : conditions) { + multiDestConditions.add((MultiDestCondition) JsonUtils.convertObject(condition, MultiDestCondition.class)); + } + multiDestConditions.sort(Comparator.comparingInt(MultiDestCondition::getPriority)); + multiDestConditionRouterRule.setConditions(multiDestConditions); + + return multiDestConditionRouterRule; + } + + public List getConditions() { + return conditions; + } + + public void setConditions(List conditions) { + this.conditions = conditions; + } +} diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/state/AbstractStateRouter.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/state/AbstractStateRouter.java index f3f1bd208ac..ee2f2e79f7a 100644 --- a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/state/AbstractStateRouter.java +++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/state/AbstractStateRouter.java @@ -125,7 +125,7 @@ public final BitList> route( routeResult = doRoute(invokers, url, invocation, needToPrintMessage, nodeHolder, messageHolder); if (routeResult != invokers) { - routeResult = invokers.and(routeResult); + routeResult = routeResult.and(invokers); } // check if router support call continue route by itself if (!supportContinueRoute()) { diff --git a/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/condition/config/ConditionStateRouterTestV31.java b/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/condition/config/ConditionStateRouterTestV31.java new file mode 100644 index 00000000000..8656d5e4b27 --- /dev/null +++ b/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/condition/config/ConditionStateRouterTestV31.java @@ -0,0 +1,403 @@ +/* + * 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.condition.config; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.config.configcenter.ConfigChangeType; +import org.apache.dubbo.common.config.configcenter.ConfigChangedEvent; +import org.apache.dubbo.common.utils.Holder; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.RpcInvocation; +import org.apache.dubbo.rpc.cluster.router.MockInvoker; +import org.apache.dubbo.rpc.cluster.router.state.BitList; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class ConditionStateRouterTestV31 { + + private static BitList> invokers; + + @BeforeAll + public static void setUp() { + + List providerUrls = Arrays.asList( + "dubbo://127.0.0.1/com.foo.BarService", + "dubbo://127.0.0.1/com.foo.BarService", + "dubbo://127.0.0.1/com.foo.BarService?env=normal", + "dubbo://127.0.0.1/com.foo.BarService?env=normal", + "dubbo://127.0.0.1/com.foo.BarService?env=normal", + "dubbo://127.0.0.1/com.foo.BarService?region=beijing", + "dubbo://127.0.0.1/com.foo.BarService?region=beijing", + "dubbo://127.0.0.1/com.foo.BarService?region=beijing", + "dubbo://127.0.0.1/com.foo.BarService?region=beijing&env=gray", + "dubbo://127.0.0.1/com.foo.BarService?region=beijing&env=gray", + "dubbo://127.0.0.1/com.foo.BarService?region=beijing&env=gray", + "dubbo://127.0.0.1/com.foo.BarService?region=beijing&env=gray", + "dubbo://127.0.0.1/com.foo.BarService?region=beijing&env=normal", + "dubbo://127.0.0.1/com.foo.BarService?region=hangzhou", + "dubbo://127.0.0.1/com.foo.BarService?region=hangzhou", + "dubbo://127.0.0.1/com.foo.BarService?region=hangzhou&env=gray", + "dubbo://127.0.0.1/com.foo.BarService?region=hangzhou&env=gray", + "dubbo://127.0.0.1/com.foo.BarService?region=hangzhou&env=normal", + "dubbo://127.0.0.1/com.foo.BarService?region=hangzhou&env=normal", + "dubbo://127.0.0.1/com.foo.BarService?region=hangzhou&env=normal", + "dubbo://dubbo.apache.org/com.foo.BarService", + "dubbo://dubbo.apache.org/com.foo.BarService", + "dubbo://dubbo.apache.org/com.foo.BarService?env=normal", + "dubbo://dubbo.apache.org/com.foo.BarService?env=normal", + "dubbo://dubbo.apache.org/com.foo.BarService?env=normal", + "dubbo://dubbo.apache.org/com.foo.BarService?region=beijing", + "dubbo://dubbo.apache.org/com.foo.BarService?region=beijing", + "dubbo://dubbo.apache.org/com.foo.BarService?region=beijing", + "dubbo://dubbo.apache.org/com.foo.BarService?region=beijing&env=gray", + "dubbo://dubbo.apache.org/com.foo.BarService?region=beijing&env=gray", + "dubbo://dubbo.apache.org/com.foo.BarService?region=beijing&env=gray", + "dubbo://dubbo.apache.org/com.foo.BarService?region=beijing&env=gray", + "dubbo://dubbo.apache.org/com.foo.BarService?region=beijing&env=normal", + "dubbo://dubbo.apache.org/com.foo.BarService?region=hangzhou", + "dubbo://dubbo.apache.org/com.foo.BarService?region=hangzhou", + "dubbo://dubbo.apache.org/com.foo.BarService?region=hangzhou&env=gray", + "dubbo://dubbo.apache.org/com.foo.BarService?region=hangzhou&env=gray", + "dubbo://dubbo.apache.org/com.foo.BarService?region=hangzhou&env=normal", + "dubbo://dubbo.apache.org/com.foo.BarService?region=hangzhou&env=normal", + "dubbo://dubbo.apache.org/com.foo.BarService?region=hangzhou&env=normal"); + + List> invokerList = providerUrls.stream() + .map(url -> new MockInvoker(URL.valueOf(url))) + .collect(Collectors.toList()); + + invokers = new BitList<>(invokerList); + } + + @Test + public void testConditionRoutePriority() throws Exception { + String config = "configVersion: v3.1\n" + "scope: service\n" + + "force: false\n" + + "runtime: true\n" + + "enabled: true\n" + + "key: shop\n" + + "conditions:\n" + + " - from:\n" + + " match:\n" + + " to:\n" + + " - match: region=$region & version=v1\n" + + " - match: region=$region & version=v2\n" + + " weight: 200\n" + + " - match: region=$region & version=v3\n" + + " weight: 300\n" + + " force: false\n" + + " ratio: 20\n" + + " priority: 20\n" + + " - from:\n" + + " match: region=beijing & version=v1\n" + + " to:\n" + + " - match: env=$env & region=beijing\n" + + " force: false\n" + + " priority: 100\n"; + + ServiceStateRouter router = new ServiceStateRouter<>( + URL.valueOf("consumer://127.0.0.1/com.foo.BarService?env=gray®ion=beijing&version=v1")); + router.process(new ConfigChangedEvent("com.foo.BarService", "", config, ConfigChangeType.ADDED)); + + RpcInvocation invocation = new RpcInvocation(); + invocation.setMethodName("getComment"); + + BitList> result = router.route( + invokers.clone(), + URL.valueOf("consumer://127.0.0.1/com.foo.BarService?env=gray®ion=beijing&version=v1"), + invocation, + false, + new Holder<>()); + + int expectedLen = 0; + for (Invoker invoker : invokers) { + if ("beijing".equals(invoker.getUrl().getParameter("region")) + && "gray".equals(invoker.getUrl().getParameter("env"))) { + expectedLen++; + } + } + + if (invokers.size() * 100 / expectedLen <= 20) { + expectedLen = 0; + } + + System.out.println("expectedLen = " + expectedLen); // expectedLen = 8 + + Assertions.assertEquals(expectedLen, result.size()); + } + + @Test + public void testConditionRouteTrafficDisable() throws Exception { + String config = "configVersion: v3.1\n" + "scope: service\n" + + "force: true\n" + + "runtime: true\n" + + "enabled: true\n" + + "key: shop\n" + + "conditions:\n" + + " - from:\n" + + " match:\n" + + " to:\n" + + " - match: region=$region & version=v1\n" + + " - match: region=$region & version=v2\n" + + " weight: 200\n" + + " - match: region=$region & version=v3\n" + + " weight: 300\n" + + " force: false\n" + + " ratio: 20\n" + + " priority: 20\n" + + " - from:\n" + + " match: region=beijing & version=v1\n" + + " to:\n" + + " force: true\n" + + " ratio: 20\n" + + " priority: 100\n"; + + ServiceStateRouter router = new ServiceStateRouter<>( + URL.valueOf("consumer://127.0.0.1/com.foo.BarService?env=gray®ion=beijing&version=v1")); + router.process(new ConfigChangedEvent("com.foo.BarService", "", config, ConfigChangeType.ADDED)); + + RpcInvocation invocation = new RpcInvocation(); + invocation.setMethodName("echo"); + + BitList> result = router.route( + invokers.clone(), + URL.valueOf("consumer://127.0.0.1/com.foo.BarService?env=gray®ion=beijing&version=v1"), + invocation, + false, + new Holder<>()); + + Assertions.assertEquals(0, result.size()); + } + + @Test + public void testConditionRouteRegionPriority() throws Exception { + String config = "configVersion: v3.1\n" + "scope: service\n" + + "force: true\n" + + "runtime: true\n" + + "enabled: true\n" + + "key: shop\n" + + "conditions:\n" + + " - from:\n" + + " match:\n" + + " to:\n" + + " - match: region=$region & env=$env\n"; + + ServiceStateRouter router = new ServiceStateRouter<>( + URL.valueOf("consumer://127.0.0.1/com.foo.BarService?env=gray®ion=beijing&version=v1")); + router.process(new ConfigChangedEvent("com.foo.BarService", "", config, ConfigChangeType.ADDED)); + + RpcInvocation invocation = new RpcInvocation(); + invocation.setMethodName("getComment"); + + BitList> result = router.route( + invokers.clone(), + URL.valueOf("consumer://127.0.0.1/com.foo.BarService?env=gray®ion=beijing&version=v1"), + invocation, + false, + new Holder<>()); + + int expectedLen = 0; + for (Invoker invoker : invokers) { + if ("gray".equals(invoker.getUrl().getParameter("env")) + && "beijing".equals(invoker.getUrl().getParameter("region"))) { + expectedLen++; + } + } + + Assertions.assertEquals(expectedLen, result.size()); + + result = router.route( + invokers.clone(), + URL.valueOf("consumer://127.0.0.1/com.foo.BarService?env=gray®ion=hangzhou"), + invocation, + false, + new Holder<>()); + expectedLen = 0; + for (Invoker invoker : invokers) { + if ("gray".equals(invoker.getUrl().getParameter("env")) + && "hangzhou".equals(invoker.getUrl().getParameter("region"))) { + expectedLen++; + } + } + + Assertions.assertEquals(expectedLen, result.size()); + + result = router.route( + invokers.clone(), + URL.valueOf("consumer://127.0.0.1/com.foo.BarService?env=normal®ion=shanghai"), + invocation, + false, + new Holder<>()); + expectedLen = 0; + for (Invoker invoker : invokers) { + if ("normal".equals(invoker.getUrl().getParameter("env")) + && "shanghai".equals(invoker.getUrl().getParameter("region"))) { + expectedLen++; + } + } + + Assertions.assertEquals(expectedLen, result.size()); + } + + @Test + public void testConditionRouteRegionPriorityFail() throws Exception { + String config = "configVersion: v3.1\n" + "scope: service\n" + + "force: true\n" + + "runtime: true\n" + + "enabled: true\n" + + "key: shop\n" + + "conditions:\n" + + " - from:\n" + + " match:\n" + + " to:\n" + + " - match: region=$region & env=$env\n" + + " ratio: 100\n"; + + ServiceStateRouter router = new ServiceStateRouter<>( + URL.valueOf("consumer://127.0.0.1/com.foo.BarService?env=gray®ion=beijing")); + router.process(new ConfigChangedEvent("com.foo.BarService", "", config, ConfigChangeType.ADDED)); + + RpcInvocation invocation = new RpcInvocation(); + invocation.setMethodName("getComment"); + + BitList> result = router.route( + invokers.clone(), + URL.valueOf("consumer://127.0.0.1/com.foo.BarService?env=gray®ion=beijing"), + invocation, + false, + new Holder<>()); + + Assertions.assertEquals(0, result.size()); + } + + @Test + public void testConditionRouteMatchFail() throws Exception { + String config = "configVersion: v3.1\n" + "scope: service\n" + + "force: false\n" + + "runtime: true\n" + + "enabled: true\n" + + "key: shop\n" + + "conditions:\n" + + " - from:\n" + + " match:\n" + + " to:\n" + + " - match: region=$region & env=$env & err-tag=Err-tag\n" + + " - from:\n" + + " match:\n" + + " trafficDisable: true\n" + + " to:\n" + + " - match:\n"; + + ServiceStateRouter router = new ServiceStateRouter<>( + URL.valueOf("consumer://127.0.0.1/com.foo.BarService?env=gray®ion=beijing")); + router.process(new ConfigChangedEvent("com.foo.BarService", "", config, ConfigChangeType.ADDED)); + + RpcInvocation invocation = new RpcInvocation(); + invocation.setMethodName("errMethod"); + + BitList> result = router.route( + invokers.clone(), + URL.valueOf("consumer://127.0.0.1/com.foo.BarService?env=gray®ion=beijing"), + invocation, + false, + new Holder<>()); + + Assertions.assertEquals(0, result.size()); + } + + @Test + public void testConditionRouteBanSpecialTraffic() throws Exception { + String config = "configVersion: v3.1\n" + "scope: service\n" + + "force: true\n" + + "runtime: true\n" + + "enabled: true\n" + + "key: shop\n" + + "conditions:\n" + + " - from:\n" + + " match: env=gray\n" + + " to:\n" + + " - match:\n" + + " force: true\n" + + " priority: 100\n" + + " - from:\n" + + " match:\n" + + " to:\n" + + " - match:\n" + + " force: true\n" + + " priority: 100\n"; + + ServiceStateRouter router = new ServiceStateRouter<>( + URL.valueOf("consumer://127.0.0.1/com.foo.BarService?env=gray®ion=beijing")); + router.process(new ConfigChangedEvent("com.foo.BarService", "", config, ConfigChangeType.ADDED)); + + RpcInvocation invocation = new RpcInvocation(); + invocation.setMethodName("errMethod"); + + BitList> result = router.route( + invokers.clone(), + URL.valueOf("consumer://127.0.0.1/com.foo.BarService?env=gray®ion=beijing"), + invocation, + false, + new Holder<>()); + + Assertions.assertEquals(invokers.size(), result.size()); + } + + @Test + public void testApplicationConditionRouteBanSpecialTraffic() throws Exception { + String config = "configVersion: v3.1\n" + "scope: application\n" + + "force: true\n" + + "runtime: true\n" + + "enabled: true\n" + + "key: shop\n" + + "conditions:\n" + + " - from:\n" + + " match: env=gray\n" + + " to:\n" + + " - match:\n" + + " force: true\n" + + " priority: 100\n" + + " - from:\n" + + " match:\n" + + " to:\n" + + " - match:\n" + + " force: true\n" + + " priority: 100\n"; + + AppStateRouter router = + new AppStateRouter<>(URL.valueOf("consumer://127.0.0.1/com.foo.BarService?env=gray®ion=beijing")); + router.process(new ConfigChangedEvent("com.foo.BarService", "", config, ConfigChangeType.ADDED)); + + RpcInvocation invocation = new RpcInvocation(); + invocation.setMethodName("errMethod"); + + BitList> result = router.route( + invokers.clone(), + URL.valueOf("consumer://127.0.0.1/com.foo.BarService?env=gray®ion=beijing"), + invocation, + false, + new Holder<>()); + + Assertions.assertEquals(invokers.size(), result.size()); + } +} diff --git a/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/condition/config/ProviderAppConditionStateRouterTest.java b/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/condition/config/ProviderAppConditionStateRouterTest.java index 7348868954b..fd7f68cb7c9 100644 --- a/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/condition/config/ProviderAppConditionStateRouterTest.java +++ b/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/condition/config/ProviderAppConditionStateRouterTest.java @@ -41,7 +41,8 @@ public class ProviderAppConditionStateRouterTest { private static GovernanceRuleRepository ruleRepository; private URL url = URL.valueOf("consumer://1.1.1.1/com.foo.BarService"); - private String rawRule = "---\n" + "configVersion: v3.0\n" + private String rawRule = "---\n" + // + "configVersion: v3.0\n" + "scope: application\n" + "force: true\n" + "runtime: false\n" diff --git a/dubbo-cluster/src/test/resources/ConditionRuleV3.1.yml b/dubbo-cluster/src/test/resources/ConditionRuleV3.1.yml new file mode 100644 index 00000000000..ddc8babf40c --- /dev/null +++ b/dubbo-cluster/src/test/resources/ConditionRuleV3.1.yml @@ -0,0 +1,55 @@ +# +# +# 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. +# +# + +configVersion: v3.1 # be v3.1 or V3.1 to use this +scope: service # must be 'service' or 'application' +key: org.apache.dubbo.samples.CommentService # [service name] or [application name] +force: false # decide hole condition route an empty set, return err Or ignore this rule +runtime: true # decide is use cache +enabled: true # decide is the rule enabled +conditions: # contains by many conditions, sort by condition.priority + + - priority: 10 # default 0, expect > 0 + from: # match consumer-side url, match fail jump next condition, match success to match provider-side urls + match: region=$region & version=v1 # string, use '&' to separate rules + trafficDisable: false # default true, if set true & from match successfully, + # it will ignore ./{'to','force','ratio'} value AND ../../{'force'} value, return empty. + to: # match provider-side urls, contains by many destination-subsets + - match: env=$env & region=shanghai # if match fail, ignore subset + weight: 100 # int, default 100, Max INT_MAX, Min 0 + - match: env=$env & region=beijing + weight: 200 + - match: env=$env & region=hangzhou + weight: 300 + force: false # here [force] decide to jump next or return empty, when get empty peer-set or ratio check false + ratio: 20 # default 0, Max 100, Min 0 -- e.g. expect $result/$all-peers >= 20% -- fail to jump next(or return empty [decide by key(force)]) + + # e.g. this condition rule will ban all traffic which sent from version=1 + - priority: 5 + from: + match: version=v1 + trafficDisable: true + + # e.g. this condition rule will show how to set region priority + - priority: 20 + from: + match: + to: + - match: region=$region + ratio: 20 diff --git a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/pom.xml b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/pom.xml index 25a3ee5e531..bcd363101b2 100644 --- a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/pom.xml +++ b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/pom.xml @@ -114,5 +114,15 @@ org.apache.logging.log4j log4j-slf4j-impl + + junit + junit + test + + + org.springframework + spring-test + test + diff --git a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/src/main/java/org/apache/dubbo/demo/consumer/Application.java b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/src/main/java/org/apache/dubbo/demo/consumer/Application.java index 2960b8d50e9..53eb3674ddc 100644 --- a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/src/main/java/org/apache/dubbo/demo/consumer/Application.java +++ b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/src/main/java/org/apache/dubbo/demo/consumer/Application.java @@ -40,8 +40,6 @@ public static void main(String[] args) throws Exception { context.start(); DemoService demoService = context.getBean("demoService", DemoService.class); GreetingService greetingService = context.getBean("greetingService", GreetingService.class); - RestDemoService restDemoService = context.getBean("restDemoService", RestDemoService.class); - TripleService tripleService = context.getBean("tripleService", TripleService.class); new Thread(() -> { while (true) { @@ -59,36 +57,6 @@ public static void main(String[] args) throws Exception { }) .start(); - new Thread(() -> { - while (true) { - try { - Object restResult = restDemoService.sayHello("rest"); - System.out.println(restResult + " from separated thread."); - restResult = restDemoService.testBody5(TestPO.getInstance()); - System.out.println(restResult + " from separated thread."); - - restResult = restDemoService.hello(1, 2); - System.out.println(restResult + " from separated thread."); - - String form1 = restDemoService.testForm1("form1"); - System.out.println(form1); - - MultivaluedHashMap multivaluedHashMap = new MultivaluedHashMap(); - multivaluedHashMap.put("1", Arrays.asList("1")); - multivaluedHashMap.put("2", Arrays.asList("2")); - MultivaluedMap form2 = restDemoService.testForm2(multivaluedHashMap); - System.out.println(form2); - } catch (Exception e) { - e.printStackTrace(); - } - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - } - } - }) - .start(); - while (true) { try { CompletableFuture hello = demoService.sayHelloAsync("world"); diff --git a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/src/main/java/org/apache/dubbo/demo/consumer/ZKTools.java b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/src/main/java/org/apache/dubbo/demo/consumer/ZKTools.java new file mode 100644 index 00000000000..c126acb3ff8 --- /dev/null +++ b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/src/main/java/org/apache/dubbo/demo/consumer/ZKTools.java @@ -0,0 +1,70 @@ +/* + * 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.demo.consumer; + +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.retry.ExponentialBackoffRetry; + +public class ZKTools { + private static String zookeeperHost = System.getProperty("zookeeper.address", "127.0.0.1"); + private static CuratorFramework client; + + public static void main(String[] args) throws Exception { + initClient(); + generateAppevelRouter(); + } + + public static void initClient() { + client = CuratorFrameworkFactory.newClient(zookeeperHost + ":2181", 60 * 1000, 60 * 1000, + new ExponentialBackoffRetry(1000, 3)); + client.start(); + } + + public static void generateAppevelRouter() { + String str = "" + + "---\n" + + "scope: application\n" + + "configVersion: v3.0\n" + + "force: true\n" + + "runtime: true\n" + + "enabled: true\n" + + "priority: 2\n" + + "key: demo-consumer\n" + + "conditions:\n" + + " - interface=org.apache.dubbo.demo.GreetingService=>address=*:20880\n" + + " - interface=org.apache.dubbo.demo.DemoService=>address=*:20881\n" + + "..."; + + System.out.println(str); + + try { + String path = "/dubbo/config/dubbo/demo-consumer.condition-router"; + if (client.checkExists().forPath(path) == null) { + client.create().creatingParentsIfNeeded().forPath(path); + } + setData(path, str); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static void setData(String path, String data) throws Exception { + client.setData().forPath(path, data.getBytes()); + } + +} diff --git a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/src/main/resources/spring/dubbo-consumer.xml b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/src/main/resources/spring/dubbo-consumer.xml index 0767baf39ab..cb26484858e 100644 --- a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/src/main/resources/spring/dubbo-consumer.xml +++ b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/src/main/resources/spring/dubbo-consumer.xml @@ -34,10 +34,4 @@ - - - - diff --git a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/src/test/java/DemoServiceIT.java b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/src/test/java/DemoServiceIT.java new file mode 100644 index 00000000000..b816e5af9cf --- /dev/null +++ b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/src/test/java/DemoServiceIT.java @@ -0,0 +1,57 @@ +/* + * 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. + */ + +import org.apache.dubbo.demo.DemoService; +import org.apache.dubbo.demo.GreetingService; +import org.apache.dubbo.demo.consumer.ZKTools; + +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = {"classpath:/spring/dubbo-consumer.xml"}) +public class DemoServiceIT { + @Autowired + private DemoService demoService; + + @Autowired + private GreetingService greetingService; + + @BeforeClass + public static void setUp() throws Exception { + ZKTools.initClient(); + ZKTools.generateAppevelRouter(); + Thread.sleep(2000); + } + + @Test + public void testDemoService() throws Exception { + String result = demoService.sayHello("world"); + Assert.assertTrue(result.contains("20881")); + } + + @Test + public void testDemoService2() throws Exception { + String result = greetingService.hello(); + Assert.assertTrue(result.contains("20880")); + } +} diff --git a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/pom.xml b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/pom.xml index 16a2a5a6a07..ddf27d3131f 100644 --- a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/pom.xml +++ b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/pom.xml @@ -107,11 +107,6 @@ dubbo-serialization-jdk ${project.version} - - org.apache.dubbo - dubbo-rpc-triple - ${project.version} - org.apache.dubbo dubbo-qos diff --git a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/java/org/apache/dubbo/demo/provider/DemoServiceImpl.java b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/java/org/apache/dubbo/demo/provider/DemoServiceImpl.java index 3467051454e..602e7830e62 100644 --- a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/java/org/apache/dubbo/demo/provider/DemoServiceImpl.java +++ b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/java/org/apache/dubbo/demo/provider/DemoServiceImpl.java @@ -37,7 +37,7 @@ public String sayHello(String name) { e.printStackTrace(); } return "Hello " + name + ", response from provider: " - + RpcContext.getServiceContext().getLocalAddress(); + + RpcContext.getServiceContext().getLocalAddress() + RpcContext.getServiceContext().getLocalPort(); } @Override diff --git a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/java/org/apache/dubbo/demo/provider/GreetingServiceImpl.java b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/java/org/apache/dubbo/demo/provider/GreetingServiceImpl.java index cc1b5de4139..83c56981943 100644 --- a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/java/org/apache/dubbo/demo/provider/GreetingServiceImpl.java +++ b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/java/org/apache/dubbo/demo/provider/GreetingServiceImpl.java @@ -17,6 +17,7 @@ package org.apache.dubbo.demo.provider; import org.apache.dubbo.demo.GreetingService; +import org.apache.dubbo.rpc.RpcContext; /** * @@ -24,6 +25,6 @@ public class GreetingServiceImpl implements GreetingService { @Override public String hello() { - return "Greetings!"; + return "Greetings!" + RpcContext.getServiceContext().getLocalPort(); } } diff --git a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/java/org/apache/dubbo/demo/provider/RestDemoServiceImpl.java b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/java/org/apache/dubbo/demo/provider/RestDemoServiceImpl.java deleted file mode 100644 index 37155fa436e..00000000000 --- a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/java/org/apache/dubbo/demo/provider/RestDemoServiceImpl.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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.demo.provider; - -import org.apache.dubbo.demo.RestDemoService; -import org.apache.dubbo.rpc.RpcContext; - -import javax.ws.rs.core.MultivaluedMap; - -import java.util.Map; - -import po.TestPO; - -public class RestDemoServiceImpl implements RestDemoService { - private static Map context; - private boolean called; - - public String sayHello(String name) { - called = true; - return "Hello, " + name; - } - - public boolean isCalled() { - return called; - } - - @Override - public Integer hello(Integer a, Integer b) { - context = RpcContext.getServerAttachment().getObjectAttachments(); - return a + b; - } - - @Override - public String error() { - throw new RuntimeException(); - } - - public static Map getAttachments() { - return context; - } - - @Override - public String getRemoteApplicationName() { - return RpcContext.getServiceContext().getRemoteApplicationName(); - } - - @Override - public Integer testBody(Integer b) { - return b; - } - - @Override - public String testBody2(String b) { - return b; - } - - @Override - public Boolean testBody2(Boolean b) { - return b; - } - - @Override - public TestPO testBody2(TestPO b) { - return b; - } - - @Override - public TestPO testBody5(TestPO testPO) { - return testPO; - } - - public String testForm1(String test) { - return test; - } - - public MultivaluedMap testForm2(MultivaluedMap map) { - return map; - } -} diff --git a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/resources/spring/dubbo-provider.xml b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/resources/spring/dubbo-provider.xml index ad402bc0683..e139f3647e1 100644 --- a/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/resources/spring/dubbo-provider.xml +++ b/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/resources/spring/dubbo-provider.xml @@ -28,21 +28,15 @@ - - - + + + - - - -