aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZHANG Dapeng <zdapeng@google.com>2020-04-04 10:48:43 -0700
committerGitHub <noreply@github.com>2020-04-04 10:48:43 -0700
commit24e3d9587eed013636d4419dc5d3dd926cbc48f0 (patch)
tree19b588f39b2244732bc676021be1ace0708ad842
parenta1815417de5142e61e3fbfc92431e2fde9054294 (diff)
downloadgrpc-grpc-java-24e3d9587eed013636d4419dc5d3dd926cbc48f0.tar.gz
xds: generate xds-routing config from XdsNameResolver
-rw-r--r--xds/build.gradle3
-rw-r--r--xds/src/main/java/io/grpc/xds/CdsLoadBalancer.java2
-rw-r--r--xds/src/main/java/io/grpc/xds/CdsLoadBalancerProvider.java3
-rw-r--r--xds/src/main/java/io/grpc/xds/EdsLoadBalancerProvider.java4
-rw-r--r--xds/src/main/java/io/grpc/xds/EnvoyProtoData.java6
-rw-r--r--xds/src/main/java/io/grpc/xds/WeightedTargetLoadBalancerProvider.java4
-rw-r--r--xds/src/main/java/io/grpc/xds/XdsClient.java35
-rw-r--r--xds/src/main/java/io/grpc/xds/XdsClientImpl.java50
-rw-r--r--xds/src/main/java/io/grpc/xds/XdsLbPolicies.java26
-rw-r--r--xds/src/main/java/io/grpc/xds/XdsNameResolver.java215
-rw-r--r--xds/src/main/java/io/grpc/xds/XdsRoutingLoadBalancerProvider.java4
-rw-r--r--xds/src/test/java/io/grpc/xds/CdsLoadBalancerTest.java2
-rw-r--r--xds/src/test/java/io/grpc/xds/XdsClientImplTest.java72
-rw-r--r--xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java232
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();
+ }
}