diff options
author | ZHANG Dapeng <zdapeng@google.com> | 2020-04-04 10:48:43 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-04-04 10:48:43 -0700 |
commit | 24e3d9587eed013636d4419dc5d3dd926cbc48f0 (patch) | |
tree | 19b588f39b2244732bc676021be1ace0708ad842 | |
parent | a1815417de5142e61e3fbfc92431e2fde9054294 (diff) | |
download | grpc-grpc-java-24e3d9587eed013636d4419dc5d3dd926cbc48f0.tar.gz |
xds: generate xds-routing config from XdsNameResolver
14 files changed, 512 insertions, 146 deletions
diff --git a/xds/build.gradle b/xds/build.gradle index 64dc866fc..f71fb3f39 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -23,7 +23,8 @@ dependencies { project(':grpc-stub'), project(':grpc-core'), project(':grpc-services'), - project(path: ':grpc-alts', configuration: 'shadow') + project(path: ':grpc-alts', configuration: 'shadow'), + libraries.gson def nettyDependency = compile project(':grpc-netty') compile (libraries.opencensus_proto) { diff --git a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer.java b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer.java index 2f995b582..81cf5dcfa 100644 --- a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer.java @@ -19,7 +19,7 @@ package io.grpc.xds; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; -import static io.grpc.xds.EdsLoadBalancerProvider.EDS_POLICY_NAME; +import static io.grpc.xds.XdsLbPolicies.EDS_POLICY_NAME; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; diff --git a/xds/src/main/java/io/grpc/xds/CdsLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/CdsLoadBalancerProvider.java index 1950168ba..bf1dbb5bc 100644 --- a/xds/src/main/java/io/grpc/xds/CdsLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/CdsLoadBalancerProvider.java @@ -36,7 +36,6 @@ import java.util.Objects; @Internal public class CdsLoadBalancerProvider extends LoadBalancerProvider { - static final String CDS_POLICY_NAME = "cds_experimental"; private static final String CLUSTER_KEY = "cluster"; @Override @@ -51,7 +50,7 @@ public class CdsLoadBalancerProvider extends LoadBalancerProvider { @Override public String getPolicyName() { - return CDS_POLICY_NAME; + return XdsLbPolicies.CDS_POLICY_NAME; } @Override diff --git a/xds/src/main/java/io/grpc/xds/EdsLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/EdsLoadBalancerProvider.java index 7985c7f1c..c7f4e12a8 100644 --- a/xds/src/main/java/io/grpc/xds/EdsLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/EdsLoadBalancerProvider.java @@ -33,8 +33,6 @@ import java.util.Map; @Internal public class EdsLoadBalancerProvider extends LoadBalancerProvider { - static final String EDS_POLICY_NAME = "eds_experimental"; - @Override public boolean isAvailable() { return true; @@ -47,7 +45,7 @@ public class EdsLoadBalancerProvider extends LoadBalancerProvider { @Override public String getPolicyName() { - return EDS_POLICY_NAME; + return XdsLbPolicies.EDS_POLICY_NAME; } @Override diff --git a/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java b/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java index 93f85d0c7..65bc3a453 100644 --- a/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java +++ b/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java @@ -18,7 +18,6 @@ package io.grpc.xds; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; -import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import io.envoyproxy.envoy.type.FractionalPercent; import io.envoyproxy.envoy.type.FractionalPercent.DenominatorType; @@ -358,8 +357,9 @@ final class EnvoyProtoData { return routeMatch; } - Optional<RouteAction> getRouteAction() { - return Optional.fromNullable(routeAction); + @Nullable + RouteAction getRouteAction() { + return routeAction; } @Override diff --git a/xds/src/main/java/io/grpc/xds/WeightedTargetLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/WeightedTargetLoadBalancerProvider.java index 8248b0bec..78ed6fe3f 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedTargetLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/WeightedTargetLoadBalancerProvider.java @@ -43,8 +43,6 @@ import javax.annotation.Nullable; @Internal public final class WeightedTargetLoadBalancerProvider extends LoadBalancerProvider { - static final String WEIGHTED_TARGET_POLICY_NAME = "weighted_target_experimental"; - @Nullable private final LoadBalancerRegistry lbRegistry; @@ -71,7 +69,7 @@ public final class WeightedTargetLoadBalancerProvider extends LoadBalancerProvid @Override public String getPolicyName() { - return WEIGHTED_TARGET_POLICY_NAME; + return XdsLbPolicies.WEIGHTED_TARGET_POLICY_NAME; } @Override diff --git a/xds/src/main/java/io/grpc/xds/XdsClient.java b/xds/src/main/java/io/grpc/xds/XdsClient.java index 59c36f57c..09187efb6 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClient.java +++ b/xds/src/main/java/io/grpc/xds/XdsClient.java @@ -17,6 +17,7 @@ package io.grpc.xds; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; @@ -63,19 +64,13 @@ abstract class XdsClient { * be used to generate a service config. */ static final class ConfigUpdate { - private final String clusterName; private final List<Route> routes; - private ConfigUpdate(String clusterName, List<Route> routes) { - this.clusterName = clusterName; + private ConfigUpdate(List<Route> routes) { this.routes = routes; } - String getClusterName() { - return clusterName; - } - - public List<Route> getRoutes() { + List<Route> getRoutes() { return routes; } @@ -84,7 +79,6 @@ abstract class XdsClient { return MoreObjects .toStringHelper(this) - .add("clusterName", clusterName) .add("routes", routes) .toString(); } @@ -95,16 +89,11 @@ abstract class XdsClient { static final class Builder { private final List<Route> routes = new ArrayList<>(); - private String clusterName; // Use ConfigUpdate.newBuilder(). private Builder() { } - Builder setClusterName(String clusterName) { - this.clusterName = clusterName; - return this; - } Builder addRoutes(Collection<Route> route) { routes.addAll(route); @@ -112,8 +101,8 @@ abstract class XdsClient { } ConfigUpdate build() { - Preconditions.checkState(clusterName != null, "clusterName is not set"); - return new ConfigUpdate(clusterName, Collections.unmodifiableList(routes)); + checkState(!routes.isEmpty(), "routes is empty"); + return new ConfigUpdate(Collections.unmodifiableList(routes)); } } } @@ -237,8 +226,8 @@ abstract class XdsClient { } ClusterUpdate build() { - Preconditions.checkState(clusterName != null, "clusterName is not set"); - Preconditions.checkState(lbPolicy != null, "lbPolicy is not set"); + checkState(clusterName != null, "clusterName is not set"); + checkState(lbPolicy != null, "lbPolicy is not set"); return new ClusterUpdate( @@ -344,7 +333,7 @@ abstract class XdsClient { } EndpointUpdate build() { - Preconditions.checkState(clusterName != null, "clusterName is not set"); + checkState(clusterName != null, "clusterName is not set"); return new EndpointUpdate( clusterName, @@ -394,7 +383,7 @@ abstract class XdsClient { } ListenerUpdate build() { - Preconditions.checkState(listener != null, "listener is not set"); + checkState(listener != null, "listener is not set"); return new ListenerUpdate(listener); } } @@ -546,7 +535,7 @@ abstract class XdsClient { @Override public synchronized XdsClient getObject() { if (xdsClient == null) { - Preconditions.checkState( + checkState( refCount == 0, "Bug: refCount should be zero while xdsClient is null"); xdsClient = xdsClientFactory.createXdsClient(); @@ -560,14 +549,14 @@ abstract class XdsClient { */ @Override public synchronized XdsClient returnObject(Object object) { - Preconditions.checkState( + checkState( object == xdsClient, "Bug: the returned object '%s' does not match current XdsClient '%s'", object, xdsClient); refCount--; - Preconditions.checkState(refCount >= 0, "Bug: refCount of XdsClient less than 0"); + checkState(refCount >= 0, "Bug: refCount of XdsClient less than 0"); if (refCount == 0) { xdsClient.shutdown(); xdsClient = null; diff --git a/xds/src/main/java/io/grpc/xds/XdsClientImpl.java b/xds/src/main/java/io/grpc/xds/XdsClientImpl.java index 2e7d33787..e963f3bdd 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClientImpl.java +++ b/xds/src/main/java/io/grpc/xds/XdsClientImpl.java @@ -24,6 +24,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Stopwatch; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.MessageOrBuilder; import com.google.protobuf.Struct; @@ -94,7 +95,8 @@ final class XdsClientImpl extends XdsClient { "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment"; // For now we do not support path matching unless enabled manually. - private static final boolean ENABLE_PATH_MATCHING = Boolean.parseBoolean( + // Mutable for testing. + static boolean enablePathMatching = Boolean.parseBoolean( System.getenv("ENABLE_EXPERIMENTAL_PATH_MATCHING")); private final MessagePrinter respPrinter = new MessagePrinter(); @@ -645,19 +647,24 @@ final class XdsClientImpl extends XdsClient { } } if (routes != null) { - // Found clusterName in the in-lined RouteConfiguration. - String clusterName = routes.get(routes.size() - 1).getRouteAction().get().getCluster(); - if (!ENABLE_PATH_MATCHING) { + // Found routes in the in-lined RouteConfiguration. + ConfigUpdate configUpdate; + if (!enablePathMatching) { + EnvoyProtoData.Route defaultRoute = Iterables.getLast(routes); + configUpdate = + ConfigUpdate.newBuilder() + .addRoutes(ImmutableList.of(defaultRoute)) + .build(); logger.log( XdsLogLevel.INFO, - "Found cluster name (inlined in route config): {0}", clusterName); + "Found cluster name (inlined in route config): {0}", + defaultRoute.getRouteAction().getCluster()); } else { + configUpdate = ConfigUpdate.newBuilder().addRoutes(routes).build(); logger.log( XdsLogLevel.INFO, "Found routes (inlined in route config): {0}", routes); } - ConfigUpdate configUpdate = ConfigUpdate.newBuilder() - .setClusterName(clusterName).addRoutes(routes).build(); configWatcher.onConfigChanged(configUpdate); } else if (rdsRouteConfigName != null) { // Send an RDS request if the resource to request has changed. @@ -816,16 +823,23 @@ final class XdsClientImpl extends XdsClient { rdsRespTimer = null; } - // Found clusterName in the in-lined RouteConfiguration. - String clusterName = routes.get(routes.size() - 1).getRouteAction().get().getCluster(); - if (!ENABLE_PATH_MATCHING) { - logger.log(XdsLogLevel.INFO, "Found cluster name: {0}", clusterName); + // Found routes in the in-lined RouteConfiguration. + ConfigUpdate configUpdate; + if (!enablePathMatching) { + EnvoyProtoData.Route defaultRoute = Iterables.getLast(routes); + configUpdate = + ConfigUpdate.newBuilder() + .addRoutes(ImmutableList.of(defaultRoute)) + .build(); + logger.log( + XdsLogLevel.INFO, + "Found cluster name: {0}", + defaultRoute.getRouteAction().getCluster()); } else { + configUpdate = ConfigUpdate.newBuilder().addRoutes(routes).build(); logger.log(XdsLogLevel.INFO, "Found {0} routes", routes.size()); logger.log(XdsLogLevel.DEBUG, "Found routes: {0}", routes); } - ConfigUpdate configUpdate = ConfigUpdate.newBuilder() - .setClusterName(clusterName).addRoutes(routes).build(); configWatcher.onConfigChanged(configUpdate); } } @@ -899,17 +913,17 @@ final class XdsClientImpl extends XdsClient { } // We only validate the default route unless path matching is enabled. - if (!ENABLE_PATH_MATCHING) { + if (!enablePathMatching) { EnvoyProtoData.Route route = routes.get(routes.size() - 1); RouteMatch routeMatch = route.getRouteMatch(); if (!routeMatch.getPath().isEmpty() || !routeMatch.getPrefix().isEmpty() || routeMatch.hasRegex()) { return "The last route must be the default route"; } - if (!route.getRouteAction().isPresent()) { + if (route.getRouteAction() == null) { return "Route action is not specified for the default route"; } - if (route.getRouteAction().get().getCluster().isEmpty()) { + if (route.getRouteAction().getCluster().isEmpty()) { return "Cluster is not specified for the default route"; } return null; @@ -925,7 +939,7 @@ final class XdsClientImpl extends XdsClient { for (int i = 0; i < routes.size(); i++) { EnvoyProtoData.Route route = routes.get(i); - if (!route.getRouteAction().isPresent()) { + if (route.getRouteAction() == null) { return "Route action is not specified for one of the routes"; } @@ -963,7 +977,7 @@ final class XdsClientImpl extends XdsClient { } } - RouteAction routeAction = route.getRouteAction().get(); + RouteAction routeAction = route.getRouteAction(); if (routeAction.getCluster().isEmpty() && routeAction.getWeightedCluster().isEmpty()) { return "Either cluster or weighted cluster route action must be provided"; } diff --git a/xds/src/main/java/io/grpc/xds/XdsLbPolicies.java b/xds/src/main/java/io/grpc/xds/XdsLbPolicies.java new file mode 100644 index 000000000..95fa120b0 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/XdsLbPolicies.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019 The gRPC Authors + * + * Licensed 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 io.grpc.xds; + +final class XdsLbPolicies { + static final String CDS_POLICY_NAME = "cds_experimental"; + static final String EDS_POLICY_NAME = "eds_experimental"; + static final String WEIGHTED_TARGET_POLICY_NAME = "weighted_target_experimental"; + static final String XDS_ROUTING_POLICY_NAME = "xds_routing_experimental"; + + private XdsLbPolicies() {} +} diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 09e8bb5de..ac59f4189 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -21,6 +21,9 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.Stopwatch; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.gson.Gson; import io.envoyproxy.envoy.api.v2.core.Node; import io.grpc.Attributes; import io.grpc.EquivalentAddressGroup; @@ -31,17 +34,21 @@ import io.grpc.Status.Code; import io.grpc.SynchronizationContext; import io.grpc.internal.BackoffPolicy; import io.grpc.internal.GrpcUtil; -import io.grpc.internal.JsonParser; import io.grpc.internal.ObjectPool; import io.grpc.xds.Bootstrapper.BootstrapInfo; import io.grpc.xds.Bootstrapper.ServerInfo; +import io.grpc.xds.EnvoyProtoData.ClusterWeight; +import io.grpc.xds.EnvoyProtoData.Route; +import io.grpc.xds.EnvoyProtoData.RouteAction; import io.grpc.xds.XdsClient.ConfigUpdate; import io.grpc.xds.XdsClient.ConfigWatcher; import io.grpc.xds.XdsClient.RefCountedXdsClientObjectPool; import io.grpc.xds.XdsClient.XdsChannelFactory; import io.grpc.xds.XdsClient.XdsClientFactory; import io.grpc.xds.XdsLogger.XdsLogLevel; -import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; @@ -96,10 +103,9 @@ final class XdsNameResolver extends NameResolver { return authority; } - @SuppressWarnings("unchecked") @Override - public void start(final Listener2 listener) { - BootstrapInfo bootstrapInfo = null; + public void start(Listener2 listener) { + BootstrapInfo bootstrapInfo; try { bootstrapInfo = bootstrapper.readBootstrap(); } catch (Exception e) { @@ -131,62 +137,163 @@ final class XdsNameResolver extends NameResolver { }; xdsClientPool = new RefCountedXdsClientObjectPool(xdsClientFactory); xdsClient = xdsClientPool.getObject(); - xdsClient.watchConfigData(authority, new ConfigWatcher() { - @Override - public void onConfigChanged(ConfigUpdate update) { + xdsClient.watchConfigData(authority, new ConfigWatcherImpl(listener)); + } + + private class ConfigWatcherImpl implements ConfigWatcher { + + final Listener2 listener; + + ConfigWatcherImpl(Listener2 listener) { + this.listener = listener; + } + + @SuppressWarnings("unchecked") + @Override + public void onConfigChanged(ConfigUpdate update) { + Map<String, ?> rawLbConfig; + if (update.getRoutes().size() > 1) { logger.log( XdsLogLevel.INFO, - "Received config update from xDS client {0}: cluster_name={1}", - xdsClient, update.getClusterName()); - String serviceConfig = "{\n" - + " \"loadBalancingConfig\": [\n" - + " {\n" - + " \"cds_experimental\": {\n" - + " \"cluster\": \"" + update.getClusterName() + "\"\n" - + " }\n" - + " }\n" - + " ]\n" - + "}"; - Map<String, ?> config; - try { - config = (Map<String, ?>) JsonParser.parse(serviceConfig); - } catch (IOException e) { - listener.onError( - Status.UNKNOWN.withDescription("Invalid service config").withCause(e)); - return; + "Received config update with {0} routes from xDS client {1}", + update.getRoutes().size(), + xdsClient); + rawLbConfig = generateXdsRoutingRawConfig(update.getRoutes()); + } else { + Route defaultRoute = Iterables.getOnlyElement(update.getRoutes()); + String clusterName = defaultRoute.getRouteAction().getCluster(); + if (!clusterName.isEmpty()) { + logger.log( + XdsLogLevel.INFO, + "Received config update from xDS client {0}: cluster_name={1}", + xdsClient, + clusterName); + rawLbConfig = generateCdsRawConfig(clusterName); + } else { + logger.log( + XdsLogLevel.INFO, + "Received config update with one weighted cluster route from xDS client {0}", + xdsClient); + List<ClusterWeight> clusterWeights = defaultRoute.getRouteAction().getWeightedCluster(); + rawLbConfig = generateWeightedTargetRawConfig(clusterWeights); } - logger.log(XdsLogLevel.INFO, "Generated service config:\n{0}", serviceConfig); - Attributes attrs = - Attributes.newBuilder() - .set(XdsAttributes.XDS_CLIENT_POOL, xdsClientPool) - .build(); - ConfigOrError parsedServiceConfig = serviceConfigParser.parseServiceConfig(config); - ResolutionResult result = - ResolutionResult.newBuilder() - .setAddresses(ImmutableList.<EquivalentAddressGroup>of()) - .setAttributes(attrs) - .setServiceConfig(parsedServiceConfig) - .build(); - listener.onResult(result); } - @Override - public void onError(Status error) { - // In order to distinguish between IO error and resource not found, which trigger - // different handling, return an empty resolution result to channel for resource not - // found. - // TODO(chengyuanzhang): Returning an empty resolution result based on status code is - // a temporary solution. More design discussion needs to be done. - if (error.getCode().equals(Code.NOT_FOUND)) { - logger.log( - XdsLogLevel.WARNING, - "Received error from xDS client {0}: {1}", xdsClient, error.getDescription()); - listener.onResult(ResolutionResult.newBuilder().build()); - return; + Map<String, ?> serviceConfig = + ImmutableMap.of("loadBalancingConfig", ImmutableList.of(rawLbConfig)); + if (logger.isLoggable(XdsLogLevel.INFO)) { + logger.log( + XdsLogLevel.INFO, + "Generated service config:\n{0}", + new Gson().toJson(serviceConfig)); + } + + Attributes attrs = + Attributes.newBuilder() + .set(XdsAttributes.XDS_CLIENT_POOL, xdsClientPool) + .build(); + ConfigOrError parsedServiceConfig = serviceConfigParser.parseServiceConfig(serviceConfig); + ResolutionResult result = + ResolutionResult.newBuilder() + .setAddresses(ImmutableList.<EquivalentAddressGroup>of()) + .setAttributes(attrs) + .setServiceConfig(parsedServiceConfig) + .build(); + listener.onResult(result); + } + + @Override + public void onError(Status error) { + // In order to distinguish between IO error and resource not found, which trigger + // different handling, return an empty resolution result to channel for resource not + // found. + // TODO(chengyuanzhang): Returning an empty resolution result based on status code is + // a temporary solution. More design discussion needs to be done. + if (error.getCode().equals(Code.NOT_FOUND)) { + logger.log( + XdsLogLevel.WARNING, + "Received error from xDS client {0}: {1}", xdsClient, error.getDescription()); + listener.onResult(ResolutionResult.newBuilder().build()); + return; + } + listener.onError(Status.UNAVAILABLE.withDescription(error.getDescription())); + } + } + + private static Map<String, ?> generateXdsRoutingRawConfig(List<Route> routesUpdate) { + List<Object> routes = new ArrayList<>(routesUpdate.size()); + Map<String, Object> actions = new LinkedHashMap<>(); + Map<RouteAction, String> exitingActions = new HashMap<>(); + for (Route route : routesUpdate) { + String service = ""; + String method = ""; + String prefix = route.getRouteMatch().getPrefix(); + String path = route.getRouteMatch().getPath(); + if (!prefix.isEmpty()) { + service = prefix.substring(1, prefix.length() - 1); + } else if (!path.isEmpty()) { + int splitIndex = path.lastIndexOf('/'); + service = path.substring(1, splitIndex); + method = path.substring(splitIndex + 1); + } + Map<String, String> methodName = ImmutableMap.of("service", service, "method", method); + String actionName; + RouteAction routeAction = route.getRouteAction(); + Map<String, ?> actionPolicy; + if (exitingActions.containsKey(routeAction)) { + actionName = exitingActions.get(routeAction); + } else { + if (!routeAction.getCluster().isEmpty()) { + actionName = "cds:" + routeAction.getCluster(); + actionPolicy = generateCdsRawConfig(routeAction.getCluster()); + } else { + StringBuilder sb = new StringBuilder("weighted:"); + List<ClusterWeight> clusterWeights = routeAction.getWeightedCluster(); + for (ClusterWeight clusterWeight : clusterWeights) { + sb.append(clusterWeight.getName()).append('_'); + } + sb.append(routeAction.hashCode()); + actionName = sb.toString(); + if (actions.containsKey(actionName)) { + // Just in case of hash collision, append exitingActions.size() to make actionName + // unique. However, in case of collision, when new ConfigUpdate is received, actions + // and actionNames might be associated differently from the previous update, but it + // is just suboptimal and won't cause a problem. + actionName = actionName + "_" + exitingActions.size(); + } + actionPolicy = generateWeightedTargetRawConfig(clusterWeights); } - listener.onError(Status.UNAVAILABLE.withDescription(error.getDescription())); + exitingActions.put(routeAction, actionName); + List<?> childPolicies = ImmutableList.of(actionPolicy); + actions.put(actionName, ImmutableMap.of("childPolicy", childPolicies)); } - }); + routes.add(ImmutableMap.of("methodName", methodName, "action", actionName)); + } + + return ImmutableMap.of( + XdsLbPolicies.XDS_ROUTING_POLICY_NAME, + ImmutableMap.of("route", routes, "action", actions)); + } + + private static Map<String, ?> generateWeightedTargetRawConfig( + List<ClusterWeight> clusterWeights) { + Map<String, Object> targets = new LinkedHashMap<>(); + for (ClusterWeight clusterWeight : clusterWeights) { + Map<String, ?> childPolicy = generateCdsRawConfig(clusterWeight.getName()); + Map<String, ?> weightedConfig = ImmutableMap.of( + "weight", + (double) clusterWeight.getWeight(), + "childPolicy", + ImmutableList.of(childPolicy)); + targets.put(clusterWeight.getName(), weightedConfig); + } + return ImmutableMap.of( + XdsLbPolicies.WEIGHTED_TARGET_POLICY_NAME, + ImmutableMap.of("targets", targets)); + } + + private static Map<String, ?> generateCdsRawConfig(String clusterName) { + return ImmutableMap.of(XdsLbPolicies.CDS_POLICY_NAME, ImmutableMap.of("cluster", clusterName)); } @Override diff --git a/xds/src/main/java/io/grpc/xds/XdsRoutingLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/XdsRoutingLoadBalancerProvider.java index 2a08ad096..2f6280769 100644 --- a/xds/src/main/java/io/grpc/xds/XdsRoutingLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/XdsRoutingLoadBalancerProvider.java @@ -48,8 +48,6 @@ import javax.annotation.Nullable; @Internal public final class XdsRoutingLoadBalancerProvider extends LoadBalancerProvider { - static final String XDS_ROUTING_POLICY_NAME = "xds_routing_experimental"; - @Nullable private final LoadBalancerRegistry lbRegistry; @@ -76,7 +74,7 @@ public final class XdsRoutingLoadBalancerProvider extends LoadBalancerProvider { @Override public String getPolicyName() { - return XDS_ROUTING_POLICY_NAME; + return XdsLbPolicies.XDS_ROUTING_POLICY_NAME; } @Override diff --git a/xds/src/test/java/io/grpc/xds/CdsLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/CdsLoadBalancerTest.java index 6de06b554..c6c2e7e34 100644 --- a/xds/src/test/java/io/grpc/xds/CdsLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/CdsLoadBalancerTest.java @@ -19,7 +19,7 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; import static io.grpc.ConnectivityState.CONNECTING; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; -import static io.grpc.xds.EdsLoadBalancerProvider.EDS_POLICY_NAME; +import static io.grpc.xds.XdsLbPolicies.EDS_POLICY_NAME; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; diff --git a/xds/src/test/java/io/grpc/xds/XdsClientImplTest.java b/xds/src/test/java/io/grpc/xds/XdsClientImplTest.java index 41c34f2fb..0b80a9ea9 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientImplTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientImplTest.java @@ -300,6 +300,7 @@ public class XdsClientImplTest { @After public void tearDown() { + XdsClientImpl.enablePathMatching = false; xdsClient.shutdown(); assertThat(adsEnded.get()).isTrue(); assertThat(lrsEnded.get()).isTrue(); @@ -488,7 +489,8 @@ public class XdsClientImplTest { ArgumentCaptor<ConfigUpdate> configUpdateCaptor = ArgumentCaptor.forClass(null); verify(configWatcher).onConfigChanged(configUpdateCaptor.capture()); - assertThat(configUpdateCaptor.getValue().getClusterName()).isEqualTo("cluster.googleapis.com"); + assertConfigUpdateContainsSingleClusterRoute( + configUpdateCaptor.getValue(), "cluster.googleapis.com"); verifyNoMoreInteractions(requestObserver); } @@ -632,7 +634,8 @@ public class XdsClientImplTest { ArgumentCaptor<ConfigUpdate> configUpdateCaptor = ArgumentCaptor.forClass(null); verify(configWatcher).onConfigChanged(configUpdateCaptor.capture()); - assertThat(configUpdateCaptor.getValue().getClusterName()).isEqualTo("cluster.googleapis.com"); + assertConfigUpdateContainsSingleClusterRoute( + configUpdateCaptor.getValue(), "cluster.googleapis.com"); } /** @@ -642,6 +645,7 @@ public class XdsClientImplTest { */ @Test public void resolveVirtualHostWithPathMatchingInRdsResponse() { + XdsClientImpl.enablePathMatching = true; xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher); StreamObserver<DiscoveryResponse> responseObserver = responseObservers.poll(); StreamObserver<DiscoveryRequest> requestObserver = requestObservers.poll(); @@ -673,8 +677,6 @@ public class XdsClientImplTest { buildRouteConfiguration( "route-foo.googleapis.com", ImmutableList.of( - buildVirtualHost(ImmutableList.of("something does not match"), - "some cluster"), VirtualHost.newBuilder() .setName("virtualhost00.googleapis.com") // don't care // domains wit a match. @@ -682,7 +684,7 @@ public class XdsClientImplTest { .addRoutes(Route.newBuilder() // path match with cluster route .setRoute(RouteAction.newBuilder().setCluster("cl1.googleapis.com")) - .setMatch(RouteMatch.newBuilder().setPath("/service1/method1/"))) + .setMatch(RouteMatch.newBuilder().setPath("/service1/method1"))) .addRoutes(Route.newBuilder() // path match with weighted cluster route .setRoute(RouteAction.newBuilder().setWeightedClusters( @@ -693,7 +695,7 @@ public class XdsClientImplTest { .addClusters(WeightedCluster.ClusterWeight.newBuilder() .setWeight(UInt32Value.newBuilder().setValue(70)) .setName("cl22.googleapis.com")))) - .setMatch(RouteMatch.newBuilder().setPath("/service2/method2/"))) + .setMatch(RouteMatch.newBuilder().setPath("/service2/method2"))) .addRoutes(Route.newBuilder() // prefix match with cluster route .setRoute(RouteAction.newBuilder() @@ -703,15 +705,7 @@ public class XdsClientImplTest { // default match with cluster route .setRoute(RouteAction.newBuilder().setCluster("cluster.googleapis.com")) .setMatch(RouteMatch.newBuilder().setPrefix(""))) - .build(), - buildVirtualHost(ImmutableList.of("something does not match"), - "some more cluster")))), - Any.pack( - buildRouteConfiguration( - "some resource name does not match route-foo.googleapis.com", - ImmutableList.of( - buildVirtualHost(ImmutableList.of("foo.googleapis.com"), - "some more cluster"))))); + .build())))); response = buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0000"); responseObserver.onNext(response); @@ -724,13 +718,12 @@ public class XdsClientImplTest { ArgumentCaptor<ConfigUpdate> configUpdateCaptor = ArgumentCaptor.forClass(null); verify(configWatcher).onConfigChanged(configUpdateCaptor.capture()); - assertThat(configUpdateCaptor.getValue().getClusterName()).isEqualTo("cluster.googleapis.com"); List<EnvoyProtoData.Route> routes = configUpdateCaptor.getValue().getRoutes(); assertThat(routes).hasSize(4); assertThat(routes.get(0)).isEqualTo( new EnvoyProtoData.Route( // path match with cluster route - new EnvoyProtoData.RouteMatch("", "/service1/method1/", false), + new EnvoyProtoData.RouteMatch("", "/service1/method1", false), new EnvoyProtoData.RouteAction( "cl1.googleapis.com", "", @@ -738,7 +731,7 @@ public class XdsClientImplTest { assertThat(routes.get(1)).isEqualTo( new EnvoyProtoData.Route( // path match with weighted cluster route - new EnvoyProtoData.RouteMatch("", "/service2/method2/", false), + new EnvoyProtoData.RouteMatch("", "/service2/method2", false), new EnvoyProtoData.RouteAction( "", "", @@ -945,7 +938,8 @@ public class XdsClientImplTest { // Cluster name is resolved and notified to config watcher. ArgumentCaptor<ConfigUpdate> configUpdateCaptor = ArgumentCaptor.forClass(null); verify(configWatcher).onConfigChanged(configUpdateCaptor.capture()); - assertThat(configUpdateCaptor.getValue().getClusterName()).isEqualTo("cluster.googleapis.com"); + assertConfigUpdateContainsSingleClusterRoute( + configUpdateCaptor.getValue(), "cluster.googleapis.com"); // Management sends back another LDS response containing updates for the requested Listener. routeConfig = @@ -973,8 +967,8 @@ public class XdsClientImplTest { // Updated cluster name is notified to config watcher. configUpdateCaptor = ArgumentCaptor.forClass(null); verify(configWatcher, times(2)).onConfigChanged(configUpdateCaptor.capture()); - assertThat(configUpdateCaptor.getValue().getClusterName()) - .isEqualTo("another-cluster.googleapis.com"); + assertConfigUpdateContainsSingleClusterRoute( + configUpdateCaptor.getValue(), "another-cluster.googleapis.com"); // Management server sends back another LDS response containing updates for the requested // Listener and telling client to do RDS. @@ -1026,8 +1020,8 @@ public class XdsClientImplTest { // Updated cluster name is notified to config watcher again. configUpdateCaptor = ArgumentCaptor.forClass(null); verify(configWatcher, times(3)).onConfigChanged(configUpdateCaptor.capture()); - assertThat(configUpdateCaptor.getValue().getClusterName()) - .isEqualTo("some-other-cluster.googleapis.com"); + assertConfigUpdateContainsSingleClusterRoute( + configUpdateCaptor.getValue(), "some-other-cluster.googleapis.com"); // Management server sends back another RDS response containing updated information for the // RouteConfiguration currently in-use by client. @@ -1049,8 +1043,8 @@ public class XdsClientImplTest { // Updated cluster name is notified to config watcher again. configUpdateCaptor = ArgumentCaptor.forClass(null); verify(configWatcher, times(4)).onConfigChanged(configUpdateCaptor.capture()); - assertThat(configUpdateCaptor.getValue().getClusterName()) - .isEqualTo("an-updated-cluster.googleapis.com"); + assertConfigUpdateContainsSingleClusterRoute( + configUpdateCaptor.getValue(), "an-updated-cluster.googleapis.com"); // Management server sends back an LDS response indicating all Listener resources are removed. response = @@ -1166,8 +1160,8 @@ public class XdsClientImplTest { // Updated cluster name is notified to config watcher. ArgumentCaptor<ConfigUpdate> configUpdateCaptor = ArgumentCaptor.forClass(null); verify(configWatcher).onConfigChanged(configUpdateCaptor.capture()); - assertThat(configUpdateCaptor.getValue().getClusterName()) - .isEqualTo("another-cluster.googleapis.com"); + assertConfigUpdateContainsSingleClusterRoute( + configUpdateCaptor.getValue(), "another-cluster.googleapis.com"); assertThat(rdsRespTimer.isCancelled()).isTrue(); } @@ -1233,7 +1227,8 @@ public class XdsClientImplTest { // Resolved cluster name is notified to config watcher. ArgumentCaptor<ConfigUpdate> configUpdateCaptor = ArgumentCaptor.forClass(null); verify(configWatcher).onConfigChanged(configUpdateCaptor.capture()); - assertThat(configUpdateCaptor.getValue().getClusterName()).isEqualTo("cluster.googleapis.com"); + assertConfigUpdateContainsSingleClusterRoute( + configUpdateCaptor.getValue(), "cluster.googleapis.com"); // Management server sends back another LDS response with the previous Listener (currently // in-use by client) removed as the RouteConfiguration it references to is absent. @@ -2709,9 +2704,10 @@ public class XdsClientImplTest { responseObserver.onNext(rdsResponse); // Client has resolved the cluster based on the RDS response. - configWatcher - .onConfigChanged( - eq(ConfigUpdate.newBuilder().setClusterName("cluster.googleapis.com").build())); + ArgumentCaptor<ConfigUpdate> configUpdateCaptor = ArgumentCaptor.forClass(null); + verify(configWatcher).onConfigChanged(configUpdateCaptor.capture()); + assertConfigUpdateContainsSingleClusterRoute( + configUpdateCaptor.getValue(), "cluster.googleapis.com"); // RPC stream closed with an error again. responseObserver.onError(Status.UNKNOWN.asException()); @@ -3374,7 +3370,7 @@ public class XdsClientImplTest { List<EnvoyProtoData.Route> routes = XdsClientImpl.findRoutesInRouteConfig(routeConfig, hostname); assertThat(routes).hasSize(1); - assertThat(routes.get(0).getRouteAction().get().getCluster()) + assertThat(routes.get(0).getRouteAction().getCluster()) .isEqualTo(targetClusterName); } @@ -3415,7 +3411,7 @@ public class XdsClientImplTest { List<EnvoyProtoData.Route> routes = XdsClientImpl.findRoutesInRouteConfig(routeConfig, hostname); assertThat(routes).hasSize(1); - assertThat(routes.get(0).getRouteAction().get().getCluster()) + assertThat(routes.get(0).getRouteAction().getCluster()) .isEqualTo(targetClusterName); } @@ -3447,7 +3443,7 @@ public class XdsClientImplTest { List<EnvoyProtoData.Route> routes = XdsClientImpl.findRoutesInRouteConfig(routeConfig, hostname); assertThat(routes).hasSize(1); - assertThat(routes.get(0).getRouteAction().get().getCluster()) + assertThat(routes.get(0).getRouteAction().getCluster()) .isEqualTo(targetClusterName); } @@ -3695,6 +3691,14 @@ public class XdsClientImplTest { assertThat(res).isEqualTo(expectedString); } + private static void assertConfigUpdateContainsSingleClusterRoute( + ConfigUpdate configUpdate, String expectedClusterName) { + List<EnvoyProtoData.Route> routes = configUpdate.getRoutes(); + assertThat(routes).hasSize(1); + assertThat(Iterables.getOnlyElement(routes).getRouteAction().getCluster()) + .isEqualTo(expectedClusterName); + } + /** * Matcher for DiscoveryRequest without the comparison of error_details field, which is used for * management server debugging purposes. diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 035c5d3e4..883ecfa1b 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -21,18 +21,28 @@ import static io.grpc.xds.XdsClientTestHelper.buildDiscoveryResponse; import static io.grpc.xds.XdsClientTestHelper.buildListener; import static io.grpc.xds.XdsClientTestHelper.buildRouteConfiguration; import static io.grpc.xds.XdsClientTestHelper.buildVirtualHost; +import static io.grpc.xds.XdsLbPolicies.CDS_POLICY_NAME; +import static io.grpc.xds.XdsLbPolicies.WEIGHTED_TARGET_POLICY_NAME; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.protobuf.Any; +import com.google.protobuf.UInt32Value; import io.envoyproxy.envoy.api.v2.DiscoveryRequest; import io.envoyproxy.envoy.api.v2.DiscoveryResponse; import io.envoyproxy.envoy.api.v2.core.AggregatedConfigSource; import io.envoyproxy.envoy.api.v2.core.ConfigSource; import io.envoyproxy.envoy.api.v2.core.Node; +import io.envoyproxy.envoy.api.v2.route.Route; +import io.envoyproxy.envoy.api.v2.route.RouteAction; +import io.envoyproxy.envoy.api.v2.route.RouteMatch; +import io.envoyproxy.envoy.api.v2.route.VirtualHost; +import io.envoyproxy.envoy.api.v2.route.WeightedCluster; +import io.envoyproxy.envoy.api.v2.route.WeightedCluster.ClusterWeight; import io.envoyproxy.envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager; import io.envoyproxy.envoy.config.filter.network.http_connection_manager.v2.Rds; import io.envoyproxy.envoy.service.discovery.v2.AggregatedDiscoveryServiceGrpc.AggregatedDiscoveryServiceImplBase; @@ -175,6 +185,7 @@ public class XdsNameResolverTest { @After public void tearDown() { xdsNameResolver.shutdown(); + XdsClientImpl.enablePathMatching = false; } @Test @@ -332,6 +343,190 @@ public class XdsNameResolverTest { @Test @SuppressWarnings("unchecked") + public void resolve_resourceUpdated_multipleRoutes() { + XdsClientImpl.enablePathMatching = true; + xdsNameResolver.start(mockListener); + assertThat(responseObservers).hasSize(1); + StreamObserver<DiscoveryResponse> responseObserver = responseObservers.poll(); + + // Simulate receiving an LDS response that contains routes resolution directly in-line. + List<Route> protoRoutes = + ImmutableList.of( + // path match, routed to cluster + Route.newBuilder() + .setMatch(buildPathMatch("fooSvc", "hello")) + .setRoute(buildClusterRoute("cluster-hello.googleapis.com")) + .build(), + // prefix match, routed to cluster + Route.newBuilder() + .setMatch(buildPrefixMatch("fooSvc")) + .setRoute(buildClusterRoute("cluster-foo.googleapis.com")) + .build(), + // path match, routed to weighted clusters + Route.newBuilder() + .setMatch(buildPathMatch("barSvc", "hello")) + .setRoute(buildWeightedClusterRoute(ImmutableMap.of( + "cluster-hello.googleapis.com", 40, "cluster-hello2.googleapis.com", 60))) + .build(), + // prefix match, routed to weighted clusters + Route.newBuilder() + .setMatch(buildPrefixMatch("barSvc")) + .setRoute( + buildWeightedClusterRoute( + ImmutableMap.of( + "cluster-bar.googleapis.com", 30, "cluster-bar2.googleapis.com", 70))) + .build(), + // default, routed to cluster + Route.newBuilder() + .setRoute(buildClusterRoute("cluster-hello.googleapis.com")) + .build()); + HttpConnectionManager httpConnectionManager = + HttpConnectionManager.newBuilder() + .setRouteConfig( + buildRouteConfiguration( + "route-foo.googleapis.com", // doesn't matter + ImmutableList.of(buildVirtualHostForRoutes(AUTHORITY, protoRoutes)))) + .build(); + List<Any> listeners = + ImmutableList.of(Any.pack(buildListener(AUTHORITY, Any.pack(httpConnectionManager)))); + responseObserver.onNext( + buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000")); + + ArgumentCaptor<ResolutionResult> resolutionResultCaptor = ArgumentCaptor.forClass(null); + verify(mockListener).onResult(resolutionResultCaptor.capture()); + ResolutionResult result = resolutionResultCaptor.getValue(); + assertThat(result.getAddresses()).isEmpty(); + Map<String, ?> serviceConfig = (Map<String, ?>) result.getServiceConfig().getConfig(); + + List<Map<String, ?>> rawLbConfigs = + (List<Map<String, ?>>) serviceConfig.get("loadBalancingConfig"); + Map<String, ?> lbConfig = Iterables.getOnlyElement(rawLbConfigs); + assertThat(lbConfig.keySet()).containsExactly("xds_routing_experimental"); + Map<String, ?> rawConfigValues = (Map<String, ?>) lbConfig.get("xds_routing_experimental"); + assertThat(rawConfigValues.keySet()).containsExactly("action", "route"); + Map<String, Map<String, ?>> actions = + (Map<String, Map<String, ?>>) rawConfigValues.get("action"); + List<Map<String, ?>> routes = (List<Map<String, ?>>) rawConfigValues.get("route"); + assertThat(routes).hasSize(5); + for (Map<String, ?> route : routes) { + assertThat(route.keySet()).containsExactly("methodName", "action"); + } + assertThat((Map<String, ?>) routes.get(0).get("methodName")) + .containsExactly("service", "fooSvc", "method", "hello"); + String action0 = (String) routes.get(0).get("action"); + assertThat((Map<String, ?>) routes.get(1).get("methodName")) + .containsExactly("service", "fooSvc", "method", ""); + String action1 = (String) routes.get(1).get("action"); + assertThat((Map<String, ?>) routes.get(2).get("methodName")) + .containsExactly("service", "barSvc", "method", "hello"); + String action2 = (String) routes.get(2).get("action"); + assertThat((Map<String, ?>) routes.get(3).get("methodName")) + .containsExactly("service", "barSvc", "method", ""); + String action3 = (String) routes.get(3).get("action"); + assertThat((Map<String, ?>) routes.get(4).get("methodName")) + .containsExactly("service", "", "method", ""); + String action4 = (String) routes.get(4).get("action"); + assertCdsPolicy(actions.get(action0), "cluster-hello.googleapis.com"); + assertCdsPolicy(actions.get(action1), "cluster-foo.googleapis.com"); + assertWeightedTargetPolicy( + actions.get(action2), + ImmutableMap.of( + "cluster-hello.googleapis.com", 40, "cluster-hello2.googleapis.com", 60)); + assertWeightedTargetPolicy( + actions.get(action3), + ImmutableMap.of( + "cluster-bar.googleapis.com", 30, "cluster-bar2.googleapis.com", 70)); + assertThat(action4).isEqualTo(action0); + + // Simulate receiving another LDS response that tells client to do RDS. + String routeConfigName = "route-foo.googleapis.com"; + responseObserver.onNext( + buildLdsResponseForRdsResource("1", AUTHORITY, routeConfigName, "0001")); + + // Client sent an RDS request for resource "route-foo.googleapis.com" (Omitted in this test). + + // Simulate receiving an RDS response that contains the resource "route-foo.googleapis.com" + // with a route resolution for a single weighted cluster route. + Route weightedClustersDefaultRoute = + Route.newBuilder() + .setRoute(buildWeightedClusterRoute( + ImmutableMap.of( + "cluster-foo.googleapis.com", 20, "cluster-bar.googleapis.com", 80))) + .build(); + List<Any> routeConfigs = ImmutableList.of( + Any.pack( + buildRouteConfiguration( + routeConfigName, + ImmutableList.of( + buildVirtualHostForRoutes( + AUTHORITY, ImmutableList.of(weightedClustersDefaultRoute)))))); + responseObserver.onNext( + buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0000")); + + verify(mockListener, times(2)).onResult(resolutionResultCaptor.capture()); + result = resolutionResultCaptor.getValue(); + assertThat(result.getAddresses()).isEmpty(); + serviceConfig = (Map<String, ?>) result.getServiceConfig().getConfig(); + rawLbConfigs = (List<Map<String, ?>>) serviceConfig.get("loadBalancingConfig"); + lbConfig = Iterables.getOnlyElement(rawLbConfigs); + assertThat(lbConfig.keySet()).containsExactly(WEIGHTED_TARGET_POLICY_NAME); + rawConfigValues = (Map<String, ?>) lbConfig.get(WEIGHTED_TARGET_POLICY_NAME); + assertWeightedTargetConfigClusterWeights( + rawConfigValues, + ImmutableMap.of( + "cluster-foo.googleapis.com", 20, "cluster-bar.googleapis.com", 80)); + } + + /** Asserts that the given action contains a single CDS policy with the given cluster name. */ + @SuppressWarnings("unchecked") + private static void assertCdsPolicy(Map<String, ?> action, String clusterName) { + assertThat(action.keySet()).containsExactly("childPolicy"); + Map<String, ?> lbConfig = + Iterables.getOnlyElement((List<Map<String, ?>>) action.get("childPolicy")); + assertThat(lbConfig.keySet()).containsExactly(CDS_POLICY_NAME); + Map<String, ?> rawConfigValues = (Map<String, ?>) lbConfig.get(CDS_POLICY_NAME); + assertThat(rawConfigValues).containsExactly("cluster", clusterName); + } + + /** + * Asserts that the given action contains a single weighted-target policy with the given cluster + * to weight mapping. + */ + @SuppressWarnings("unchecked") + private static void assertWeightedTargetPolicy( + Map<String, ?> action, Map<String, Integer> clusterWeights) { + assertThat(action.keySet()).containsExactly("childPolicy"); + Map<String, ?> lbConfig = + Iterables.getOnlyElement((List<Map<String, ?>>) action.get("childPolicy")); + assertThat(lbConfig.keySet()).containsExactly(WEIGHTED_TARGET_POLICY_NAME); + Map<String, ?> rawConfigValues = (Map<String, ?>) lbConfig.get(WEIGHTED_TARGET_POLICY_NAME); + assertWeightedTargetConfigClusterWeights(rawConfigValues, clusterWeights); + } + + /** + * Asserts that the given raw config is a weighted-target config with the given cluster to weight + * mapping. + */ + @SuppressWarnings("unchecked") + private static void assertWeightedTargetConfigClusterWeights( + Map<String, ?> rawConfigValues, Map<String, Integer> clusterWeight) { + assertThat(rawConfigValues.keySet()).containsExactly("targets"); + Map<String, ?> targets = (Map<String, ?>) rawConfigValues.get("targets"); + assertThat(targets.keySet()).isEqualTo(clusterWeight.keySet()); + for (String targetName : targets.keySet()) { + Map<String, ?> target = (Map<String, ?>) targets.get(targetName); + assertThat(target.keySet()).containsExactly("childPolicy", "weight"); + Map<String, ?> lbConfig = + Iterables.getOnlyElement((List<Map<String, ?>>) target.get("childPolicy")); + assertThat(lbConfig.keySet()).containsExactly(CDS_POLICY_NAME); + Map<String, ?> rawClusterConfigValues = (Map<String, ?>) lbConfig.get(CDS_POLICY_NAME); + assertThat(rawClusterConfigValues).containsExactly("cluster", targetName); + assertThat(target.get("weight")).isEqualTo(clusterWeight.get(targetName)); + } + } + + @Test + @SuppressWarnings("unchecked") public void resolve_resourceNewlyAdded() { xdsNameResolver.start(mockListener); assertThat(responseObservers).hasSize(1); @@ -426,4 +621,41 @@ public class XdsNameResolverTest { buildVirtualHost(ImmutableList.of(host), clusterName))))); return buildDiscoveryResponse(versionInfo, routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, nonce); } + + private static RouteMatch buildPrefixMatch(String service) { + return RouteMatch.newBuilder().setPrefix("/" + service + "/").build(); + } + + private static RouteMatch buildPathMatch(String service, String method) { + return RouteMatch.newBuilder().setPath("/" + service + "/" + method).build(); + } + + private static RouteAction buildClusterRoute(String clusterName) { + return RouteAction.newBuilder().setCluster(clusterName).build(); + } + + /** + * Builds a RouteAction for a weighted cluster route. The given map is keyed by cluster name and + * valued by the weight of the cluster. + */ + private static RouteAction buildWeightedClusterRoute(Map<String, Integer> clusterWeights) { + WeightedCluster.Builder builder = WeightedCluster.newBuilder(); + for (Map.Entry<String, Integer> entry : clusterWeights.entrySet()) { + builder.addClusters( + ClusterWeight.newBuilder() + .setName(entry.getKey()) + .setWeight(UInt32Value.newBuilder().setValue(entry.getValue()))); + } + return RouteAction.newBuilder() + .setWeightedClusters(builder) + .build(); + } + + private static VirtualHost buildVirtualHostForRoutes(String domain, List<Route> routes) { + return VirtualHost.newBuilder() + .setName("virtualhost00.googleapis.com") // don't care + .addAllDomains(ImmutableList.of(domain)) + .addAllRoutes(routes) + .build(); + } } |