xds: implement xds_routing policy config generation and parsing (#7055)

Implemented service config generation in the xDS resolver with xds_routing that supports all matchers. Reimplemented xds_routing config parsing with all matchers. Existing implementation for xds_routing LB policy is mostly deleted for now, as we would need to reimplement the route matching logic with matchers added.
This commit is contained in:
Chengyuan Zhang 2020-06-04 09:03:49 +00:00 committed by GitHub
parent 417d7700dd
commit c551fe3807
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1705 additions and 1569 deletions

View File

@ -27,6 +27,9 @@ import com.google.re2j.PatternSyntaxException;
import io.envoyproxy.envoy.type.FractionalPercent;
import io.envoyproxy.envoy.type.FractionalPercent.DenominatorType;
import io.grpc.EquivalentAddressGroup;
import io.grpc.xds.RouteMatch.FractionMatcher;
import io.grpc.xds.RouteMatch.HeaderMatcher;
import io.grpc.xds.RouteMatch.PathMatcher;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
@ -446,7 +449,12 @@ final class EnvoyProtoData {
// TODO(chengyuanzhang): delete and do not use after routing feature is always ON.
boolean isDefaultRoute() {
return routeMatch.isMatchAll();
// For backward compatibility, all the other matchers are ignored.
String prefix = routeMatch.getPathMatch().getPrefix();
if (prefix != null) {
return prefix.isEmpty() || prefix.equals("/");
}
return false;
}
@Override
@ -477,7 +485,7 @@ final class EnvoyProtoData {
@Nullable
static StructOrError<Route> fromEnvoyProtoRoute(io.envoyproxy.envoy.api.v2.route.Route proto) {
StructOrError<RouteMatch> routeMatch = RouteMatch.fromEnvoyProtoRouteMatch(proto.getMatch());
StructOrError<RouteMatch> routeMatch = convertEnvoyProtoRouteMatch(proto.getMatch());
if (routeMatch == null) {
return null;
}
@ -510,108 +518,11 @@ final class EnvoyProtoData {
}
return StructOrError.fromStruct(new Route(routeMatch.getStruct(), routeAction.getStruct()));
}
}
/** See corresponding Envoy proto message {@link io.envoyproxy.envoy.api.v2.route.RouteMatch}. */
static final class RouteMatch {
// Exactly one of the following fields is non-null.
@Nullable
private final String pathPrefixMatch;
@Nullable
private final String pathExactMatch;
@Nullable
private final Pattern pathSafeRegExMatch;
private final List<HeaderMatcher> headerMatchers;
@Nullable
private final Fraction fractionMatch;
@VisibleForTesting
RouteMatch(
@Nullable String pathPrefixMatch, @Nullable String pathExactMatch,
@Nullable Pattern pathSafeRegExMatch, @Nullable Fraction fractionMatch,
List<HeaderMatcher> headerMatchers) {
this.pathPrefixMatch = pathPrefixMatch;
this.pathExactMatch = pathExactMatch;
this.pathSafeRegExMatch = pathSafeRegExMatch;
this.fractionMatch = fractionMatch;
this.headerMatchers = headerMatchers;
}
RouteMatch(@Nullable String pathPrefixMatch, @Nullable String pathExactMatch) {
this(
pathPrefixMatch, pathExactMatch, null, null,
Collections.<HeaderMatcher>emptyList());
}
@Nullable
String getPathPrefixMatch() {
return pathPrefixMatch;
}
@Nullable
String getPathExactMatch() {
return pathExactMatch;
}
// TODO(chengyuanzhang): delete and do not use after routing feature is always ON.
private boolean isMatchAll() {
// For backward compatibility, all the other matchers are ignored. When routing is enabled,
// we should never care if a matcher matches all requests.
if (pathPrefixMatch != null) {
return pathPrefixMatch.isEmpty() || pathPrefixMatch.equals("/");
}
return false;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RouteMatch that = (RouteMatch) o;
return Objects.equals(pathPrefixMatch, that.pathPrefixMatch)
&& Objects.equals(pathExactMatch, that.pathExactMatch)
&& Objects.equals(
pathSafeRegExMatch == null ? null : pathSafeRegExMatch.pattern(),
that.pathSafeRegExMatch == null ? null : that.pathSafeRegExMatch.pattern())
&& Objects.equals(fractionMatch, that.fractionMatch)
&& Objects.equals(headerMatchers, that.headerMatchers);
}
@Override
public int hashCode() {
return Objects.hash(
pathPrefixMatch, pathExactMatch,
pathSafeRegExMatch == null ? null : pathSafeRegExMatch.pattern(), headerMatchers,
fractionMatch);
}
@Override
public String toString() {
ToStringHelper toStringHelper = MoreObjects.toStringHelper(this);
if (pathPrefixMatch != null) {
toStringHelper.add("pathPrefixMatch", pathPrefixMatch);
}
if (pathExactMatch != null) {
toStringHelper.add("pathExactMatch", pathExactMatch);
}
if (pathSafeRegExMatch != null) {
toStringHelper.add("pathSafeRegExMatch",pathSafeRegExMatch.pattern());
}
if (fractionMatch != null) {
toStringHelper.add("fractionMatch", fractionMatch);
}
return toStringHelper.add("headerMatchers", headerMatchers).toString();
}
@VisibleForTesting
@SuppressWarnings("deprecation")
@Nullable
static StructOrError<RouteMatch> fromEnvoyProtoRouteMatch(
static StructOrError<RouteMatch> convertEnvoyProtoRouteMatch(
io.envoyproxy.envoy.api.v2.route.RouteMatch proto) {
if (proto.getQueryParametersCount() != 0) {
return null;
@ -620,59 +531,24 @@ final class EnvoyProtoData {
return StructOrError.fromError("Unsupported match option: case insensitive");
}
Fraction fraction = null;
if (proto.hasRuntimeFraction()) {
io.envoyproxy.envoy.type.FractionalPercent percent =
proto.getRuntimeFraction().getDefaultValue();
int numerator = percent.getNumerator();
int denominator = 0;
switch (percent.getDenominator()) {
case HUNDRED:
denominator = 100;
break;
case TEN_THOUSAND:
denominator = 10_000;
break;
case MILLION:
denominator = 1_000_000;
break;
case UNRECOGNIZED:
default:
return StructOrError.fromError(
"Unrecognized fractional percent denominator: " + percent.getDenominator());
}
fraction = new Fraction(numerator, denominator);
StructOrError<PathMatcher> pathMatch = convertEnvoyProtoPathMatcher(proto);
if (pathMatch.getErrorDetail() != null) {
return StructOrError.fromError(pathMatch.getErrorDetail());
}
String prefixPathMatch = null;
String exactPathMatch = null;
Pattern safeRegExPathMatch = null;
switch (proto.getPathSpecifierCase()) {
case PREFIX:
prefixPathMatch = proto.getPrefix();
break;
case PATH:
exactPathMatch = proto.getPath();
break;
case REGEX:
return StructOrError.fromError("Unsupported path match type: regex");
case SAFE_REGEX:
String rawPattern = proto.getSafeRegex().getRegex();
try {
safeRegExPathMatch = Pattern.compile(rawPattern);
} catch (PatternSyntaxException e) {
return StructOrError.fromError("Malformed safe regex pattern: " + e.getMessage());
}
break;
case PATHSPECIFIER_NOT_SET:
default:
return StructOrError.fromError("Unknown path match type");
FractionMatcher fractionMatch = null;
if (proto.hasRuntimeFraction()) {
StructOrError<FractionMatcher> parsedFraction =
convertEnvoyProtoFraction(proto.getRuntimeFraction().getDefaultValue());
if (parsedFraction.getErrorDetail() != null) {
return StructOrError.fromError(parsedFraction.getErrorDetail());
}
fractionMatch = parsedFraction.getStruct();
}
List<HeaderMatcher> headerMatchers = new ArrayList<>();
for (io.envoyproxy.envoy.api.v2.route.HeaderMatcher hmProto : proto.getHeadersList()) {
StructOrError<HeaderMatcher> headerMatcher =
HeaderMatcher.fromEnvoyProtoHeaderMatcher(hmProto);
StructOrError<HeaderMatcher> headerMatcher = convertEnvoyProtoHeaderMatcher(hmProto);
if (headerMatcher.getErrorDetail() != null) {
return StructOrError.fromError(headerMatcher.getErrorDetail());
}
@ -681,96 +557,68 @@ final class EnvoyProtoData {
return StructOrError.fromStruct(
new RouteMatch(
prefixPathMatch, exactPathMatch, safeRegExPathMatch, fraction,
Collections.unmodifiableList(headerMatchers)));
pathMatch.getStruct(), Collections.unmodifiableList(headerMatchers), fractionMatch));
}
static final class Fraction {
private final int numerator;
private final int denominator;
@VisibleForTesting
Fraction(int numerator, int denominator) {
this.numerator = numerator;
this.denominator = denominator;
@SuppressWarnings("deprecation")
private static StructOrError<PathMatcher> convertEnvoyProtoPathMatcher(
io.envoyproxy.envoy.api.v2.route.RouteMatch proto) {
String path = null;
String prefix = null;
Pattern safeRegEx = null;
switch (proto.getPathSpecifierCase()) {
case PREFIX:
prefix = proto.getPrefix();
break;
case PATH:
path = proto.getPath();
break;
case REGEX:
return StructOrError.fromError("Unsupported path match type: regex");
case SAFE_REGEX:
String rawPattern = proto.getSafeRegex().getRegex();
try {
safeRegEx = Pattern.compile(rawPattern);
} catch (PatternSyntaxException e) {
return StructOrError.fromError("Malformed safe regex pattern: " + e.getMessage());
}
break;
case PATHSPECIFIER_NOT_SET:
default:
return StructOrError.fromError("Unknown path match type");
}
@Override
public int hashCode() {
return Objects.hash(numerator, denominator);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Fraction that = (Fraction) o;
return Objects.equals(numerator, that.numerator)
&& Objects.equals(denominator, that.denominator);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("numerator", numerator)
.add("denominator", denominator)
.toString();
}
}
}
/**
* See corresponding Envoy proto message {@link io.envoyproxy.envoy.api.v2.route.HeaderMatcher}.
*/
@SuppressWarnings("unused")
static final class HeaderMatcher {
private final String name;
// Exactly one of the following fields is non-null.
@Nullable
private final String exactMatch;
@Nullable
private final Pattern safeRegExMatch;
@Nullable
private final Range rangeMatch;
@Nullable
private final Boolean presentMatch;
@Nullable
private final String prefixMatch;
@Nullable
private final String suffixMatch;
private final boolean isInvertedMatch;
@VisibleForTesting
HeaderMatcher(
String name,
@Nullable String exactMatch, @Nullable Pattern safeRegExMatch, @Nullable Range rangeMatch,
@Nullable Boolean presentMatch, @Nullable String prefixMatch, @Nullable String suffixMatch,
boolean isInvertedMatch) {
this.name = name;
this.exactMatch = exactMatch;
this.safeRegExMatch = safeRegExMatch;
this.rangeMatch = rangeMatch;
this.presentMatch = presentMatch;
this.prefixMatch = prefixMatch;
this.suffixMatch = suffixMatch;
this.isInvertedMatch = isInvertedMatch;
return StructOrError.fromStruct(new PathMatcher(path, prefix, safeRegEx));
}
// TODO (chengyuanzhang): add getters when needed.
private static StructOrError<FractionMatcher> convertEnvoyProtoFraction(
io.envoyproxy.envoy.type.FractionalPercent proto) {
int numerator = proto.getNumerator();
int denominator = 0;
switch (proto.getDenominator()) {
case HUNDRED:
denominator = 100;
break;
case TEN_THOUSAND:
denominator = 10_000;
break;
case MILLION:
denominator = 1_000_000;
break;
case UNRECOGNIZED:
default:
return StructOrError.fromError(
"Unrecognized fractional percent denominator: " + proto.getDenominator());
}
return StructOrError.fromStruct(new FractionMatcher(numerator, denominator));
}
@VisibleForTesting
@SuppressWarnings("deprecation")
static StructOrError<HeaderMatcher> fromEnvoyProtoHeaderMatcher(
static StructOrError<HeaderMatcher> convertEnvoyProtoHeaderMatcher(
io.envoyproxy.envoy.api.v2.route.HeaderMatcher proto) {
String exactMatch = null;
Pattern safeRegExMatch = null;
Range rangeMatch = null;
HeaderMatcher.Range rangeMatch = null;
Boolean presentMatch = null;
String prefixMatch = null;
String suffixMatch = null;
@ -793,7 +641,9 @@ final class EnvoyProtoData {
}
break;
case RANGE_MATCH:
rangeMatch = new Range(proto.getRangeMatch().getStart(), proto.getRangeMatch().getEnd());
rangeMatch =
new HeaderMatcher.Range(
proto.getRangeMatch().getStart(), proto.getRangeMatch().getEnd());
break;
case PRESENT_MATCH:
presentMatch = proto.getPresentMatch();
@ -813,96 +663,6 @@ final class EnvoyProtoData {
proto.getName(), exactMatch, safeRegExMatch, rangeMatch, presentMatch,
prefixMatch, suffixMatch, proto.getInvertMatch()));
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
HeaderMatcher that = (HeaderMatcher) o;
return Objects.equals(name, that.name)
&& Objects.equals(exactMatch, that.exactMatch)
&& Objects.equals(
safeRegExMatch == null ? null : safeRegExMatch.pattern(),
that.safeRegExMatch == null ? null : that.safeRegExMatch.pattern())
&& Objects.equals(rangeMatch, that.rangeMatch)
&& Objects.equals(presentMatch, that.presentMatch)
&& Objects.equals(prefixMatch, that.prefixMatch)
&& Objects.equals(suffixMatch, that.suffixMatch)
&& Objects.equals(isInvertedMatch, that.isInvertedMatch);
}
@Override
public int hashCode() {
return Objects.hash(
name, exactMatch, safeRegExMatch == null ? null : safeRegExMatch.pattern(),
rangeMatch, presentMatch, prefixMatch, suffixMatch, isInvertedMatch);
}
@Override
public String toString() {
ToStringHelper toStringHelper =
MoreObjects.toStringHelper(this).add("name", name);
if (exactMatch != null) {
toStringHelper.add("exactMatch", exactMatch);
}
if (safeRegExMatch != null) {
toStringHelper.add("safeRegExMatch", safeRegExMatch.pattern());
}
if (rangeMatch != null) {
toStringHelper.add("rangeMatch", rangeMatch);
}
if (presentMatch != null) {
toStringHelper.add("presentMatch", presentMatch);
}
if (prefixMatch != null) {
toStringHelper.add("prefixMatch", prefixMatch);
}
if (suffixMatch != null) {
toStringHelper.add("suffixMatch", suffixMatch);
}
return toStringHelper.add("isInvertedMatch", isInvertedMatch).toString();
}
static final class Range {
private final long start;
private final long end;
@VisibleForTesting
Range(long start, long end) {
this.start = start;
this.end = end;
}
@Override
public int hashCode() {
return Objects.hash(start, end);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Range that = (Range) o;
return Objects.equals(start, that.start)
&& Objects.equals(end, that.end);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("start", start)
.add("end", end)
.toString();
}
}
}
/** See corresponding Envoy proto message {@link io.envoyproxy.envoy.api.v2.route.RouteAction}. */
@ -979,6 +739,7 @@ final class EnvoyProtoData {
: clusterWeights) {
weightedClusters.add(ClusterWeight.fromEnvoyProtoClusterWeight(clusterWeight));
}
// TODO(chengyuanzhang): validate if the sum of weights equals to total weight.
break;
case CLUSTERSPECIFIER_NOT_SET:
default:

View File

@ -0,0 +1,372 @@
/*
* Copyright 2020 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;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.re2j.Pattern;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javax.annotation.Nullable;
/**
* A {@link RouteMatch} represents a group of routing rules used by a logical route to filter RPCs.
*/
final class RouteMatch {
private final PathMatcher pathMatch;
private final List<HeaderMatcher> headerMatchers;
@Nullable
private final FractionMatcher fractionMatch;
@VisibleForTesting
RouteMatch(PathMatcher pathMatch, List<HeaderMatcher> headerMatchers,
@Nullable FractionMatcher fractionMatch) {
this.pathMatch = pathMatch;
this.fractionMatch = fractionMatch;
this.headerMatchers = headerMatchers;
}
RouteMatch(@Nullable String pathPrefixMatch, @Nullable String pathExactMatch) {
this(
new PathMatcher(pathExactMatch, pathPrefixMatch, null),
Collections.<HeaderMatcher>emptyList(), null);
}
PathMatcher getPathMatch() {
return pathMatch;
}
List<HeaderMatcher> getHeaderMatchers() {
return Collections.unmodifiableList(headerMatchers);
}
@Nullable
FractionMatcher getFractionMatch() {
return fractionMatch;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RouteMatch that = (RouteMatch) o;
return Objects.equals(pathMatch, that.pathMatch)
&& Objects.equals(fractionMatch, that.fractionMatch)
&& Objects.equals(headerMatchers, that.headerMatchers);
}
@Override
public int hashCode() {
return Objects.hash(pathMatch, fractionMatch, headerMatchers);
}
@Override
public String toString() {
ToStringHelper toStringHelper =
MoreObjects.toStringHelper(this).add("pathMatch", pathMatch);
if (fractionMatch != null) {
toStringHelper.add("fractionMatch", fractionMatch);
}
return toStringHelper.add("headerMatchers", headerMatchers).toString();
}
static final class PathMatcher {
// Exactly one of the following fields is non-null.
@Nullable
private final String path;
@Nullable
private final String prefix;
@Nullable
private final Pattern regEx;
PathMatcher(@Nullable String path, @Nullable String prefix, @Nullable Pattern regEx) {
this.path = path;
this.prefix = prefix;
this.regEx = regEx;
}
@Nullable
String getPath() {
return path;
}
@Nullable
String getPrefix() {
return prefix;
}
@Nullable
Pattern getRegEx() {
return regEx;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PathMatcher that = (PathMatcher) o;
return Objects.equals(path, that.path)
&& Objects.equals(prefix, that.prefix)
&& Objects.equals(
regEx == null ? null : regEx.pattern(),
that.regEx == null ? null : that.regEx.pattern());
}
@Override
public int hashCode() {
return Objects.hash(path, prefix, regEx == null ? null : regEx.pattern());
}
@Override
public String toString() {
ToStringHelper toStringHelper =
MoreObjects.toStringHelper(this);
if (path != null) {
toStringHelper.add("path", path);
}
if (prefix != null) {
toStringHelper.add("prefix", prefix);
}
if (regEx != null) {
toStringHelper.add("regEx", regEx.pattern());
}
return toStringHelper.toString();
}
}
/**
* Matching rules for a specific HTTP/2 header.
*/
static final class HeaderMatcher {
private final String name;
// Exactly one of the following fields is non-null.
@Nullable
private final String exactMatch;
@Nullable
private final Pattern safeRegExMatch;
@Nullable
private final Range rangeMatch;
@Nullable
private final Boolean presentMatch;
@Nullable
private final String prefixMatch;
@Nullable
private final String suffixMatch;
private final boolean isInvertedMatch;
// TODO(chengyuanzhang): use builder to enforce oneof semantics would be better.
HeaderMatcher(
String name,
@Nullable String exactMatch, @Nullable Pattern safeRegExMatch, @Nullable Range rangeMatch,
@Nullable Boolean presentMatch, @Nullable String prefixMatch, @Nullable String suffixMatch,
boolean isInvertedMatch) {
this.name = name;
this.exactMatch = exactMatch;
this.safeRegExMatch = safeRegExMatch;
this.rangeMatch = rangeMatch;
this.presentMatch = presentMatch;
this.prefixMatch = prefixMatch;
this.suffixMatch = suffixMatch;
this.isInvertedMatch = isInvertedMatch;
}
String getName() {
return name;
}
String getExactMatch() {
return exactMatch;
}
Pattern getRegExMatch() {
return safeRegExMatch;
}
Range getRangeMatch() {
return rangeMatch;
}
Boolean getPresentMatch() {
return presentMatch;
}
String getPrefixMatch() {
return prefixMatch;
}
String getSuffixMatch() {
return suffixMatch;
}
boolean isInvertedMatch() {
return isInvertedMatch;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
HeaderMatcher that = (HeaderMatcher) o;
return Objects.equals(name, that.name)
&& Objects.equals(exactMatch, that.exactMatch)
&& Objects.equals(
safeRegExMatch == null ? null : safeRegExMatch.pattern(),
that.safeRegExMatch == null ? null : that.safeRegExMatch.pattern())
&& Objects.equals(rangeMatch, that.rangeMatch)
&& Objects.equals(presentMatch, that.presentMatch)
&& Objects.equals(prefixMatch, that.prefixMatch)
&& Objects.equals(suffixMatch, that.suffixMatch)
&& Objects.equals(isInvertedMatch, that.isInvertedMatch);
}
@Override
public int hashCode() {
return Objects.hash(
name, exactMatch, safeRegExMatch == null ? null : safeRegExMatch.pattern(),
rangeMatch, presentMatch, prefixMatch, suffixMatch, isInvertedMatch);
}
@Override
public String toString() {
ToStringHelper toStringHelper =
MoreObjects.toStringHelper(this).add("name", name);
if (exactMatch != null) {
toStringHelper.add("exactMatch", exactMatch);
}
if (safeRegExMatch != null) {
toStringHelper.add("safeRegExMatch", safeRegExMatch.pattern());
}
if (rangeMatch != null) {
toStringHelper.add("rangeMatch", rangeMatch);
}
if (presentMatch != null) {
toStringHelper.add("presentMatch", presentMatch);
}
if (prefixMatch != null) {
toStringHelper.add("prefixMatch", prefixMatch);
}
if (suffixMatch != null) {
toStringHelper.add("suffixMatch", suffixMatch);
}
return toStringHelper.add("isInvertedMatch", isInvertedMatch).toString();
}
static final class Range {
private final long start;
private final long end;
Range(long start, long end) {
this.start = start;
this.end = end;
}
long getStart() {
return start;
}
long getEnd() {
return end;
}
@Override
public int hashCode() {
return Objects.hash(start, end);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Range that = (Range) o;
return Objects.equals(start, that.start)
&& Objects.equals(end, that.end);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("start", start)
.add("end", end)
.toString();
}
}
}
static final class FractionMatcher {
private final int numerator;
private final int denominator;
FractionMatcher(int numerator, int denominator) {
this.numerator = numerator;
this.denominator = denominator;
}
int getNumerator() {
return numerator;
}
int getDenominator() {
return denominator;
}
@Override
public int hashCode() {
return Objects.hash(numerator, denominator);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
FractionMatcher that = (FractionMatcher) o;
return Objects.equals(numerator, that.numerator)
&& Objects.equals(denominator, that.denominator);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("numerator", numerator)
.add("denominator", denominator)
.toString();
}
}
}

View File

@ -18,12 +18,14 @@ package io.grpc.xds;
import static com.google.common.base.Preconditions.checkNotNull;
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.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.gson.Gson;
import com.google.re2j.Pattern;
import io.envoyproxy.envoy.api.v2.core.Node;
import io.grpc.Attributes;
import io.grpc.EquivalentAddressGroup;
@ -39,6 +41,9 @@ 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.RouteMatch.FractionMatcher;
import io.grpc.xds.RouteMatch.HeaderMatcher;
import io.grpc.xds.RouteMatch.PathMatcher;
import io.grpc.xds.XdsClient.ConfigUpdate;
import io.grpc.xds.XdsClient.ConfigWatcher;
import io.grpc.xds.XdsClient.RefCountedXdsClientObjectPool;
@ -169,18 +174,13 @@ final class XdsNameResolver extends NameResolver {
xdsClient,
clusterName);
rawLbConfig = generateCdsRawConfig(clusterName);
} else if (action.getWeightedCluster() != null) {
} 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);
} else {
// TODO (chengyuanzhang): route with cluster_header
logger.log(
XdsLogLevel.WARNING, "Route action with cluster_header is not implemented");
return;
}
}
@ -228,38 +228,22 @@ final class XdsNameResolver extends NameResolver {
}
}
private static ImmutableMap<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 = "";
if (!route.isDefaultRoute()) {
String prefix = route.getRouteMatch().getPathPrefixMatch();
String path = route.getRouteMatch().getPathExactMatch();
if (prefix != null) {
service = prefix.substring(1, prefix.length() - 1);
} else if (path != null) {
int splitIndex = path.lastIndexOf('/');
service = path.substring(1, splitIndex);
method = path.substring(splitIndex + 1);
} else {
// TODO (chengyuanzhang): match with regex.
continue;
}
}
Map<String, String> methodName = ImmutableMap.of("service", service, "method", method);
String actionName;
@VisibleForTesting
static ImmutableMap<String, ?> generateXdsRoutingRawConfig(List<Route> routes) {
List<Object> rawRoutes = new ArrayList<>();
Map<String, Object> rawActions = new LinkedHashMap<>();
Map<RouteAction, String> existingActions = new HashMap<>();
for (Route route : routes) {
RouteAction routeAction = route.getRouteAction();
String actionName;
Map<String, ?> actionPolicy;
if (exitingActions.containsKey(routeAction)) {
actionName = exitingActions.get(routeAction);
if (existingActions.containsKey(routeAction)) {
actionName = existingActions.get(routeAction);
} else {
if (routeAction.getCluster() != null) {
actionName = "cds:" + routeAction.getCluster();
actionPolicy = generateCdsRawConfig(routeAction.getCluster());
} else if (routeAction.getWeightedCluster() != null) {
} else {
StringBuilder sb = new StringBuilder("weighted:");
List<ClusterWeight> clusterWeights = routeAction.getWeightedCluster();
for (ClusterWeight clusterWeight : clusterWeights) {
@ -267,31 +251,103 @@ final class XdsNameResolver extends NameResolver {
}
sb.append(routeAction.hashCode());
actionName = sb.toString();
if (actions.containsKey(actionName)) {
// Just in case of hash collision, append exitingActions.size() to make actionName
if (rawActions.containsKey(actionName)) {
// Just in case of hash collision, append existingActions.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();
actionName = actionName + "_" + existingActions.size();
}
actionPolicy = generateWeightedTargetRawConfig(clusterWeights);
} else {
// TODO (chengyuanzhang): route with cluster_header.
continue;
}
exitingActions.put(routeAction, actionName);
existingActions.put(routeAction, actionName);
List<?> childPolicies = ImmutableList.of(actionPolicy);
actions.put(actionName, ImmutableMap.of("childPolicy", childPolicies));
rawActions.put(actionName, ImmutableMap.of("childPolicy", childPolicies));
}
routes.add(ImmutableMap.of("methodName", methodName, "action", actionName));
ImmutableMap<String, ?> configRoute = convertToRawRoute(route.getRouteMatch(), actionName);
rawRoutes.add(configRoute);
}
return ImmutableMap.of(
XdsLbPolicies.XDS_ROUTING_POLICY_NAME,
ImmutableMap.of("route", routes, "action", actions));
ImmutableMap.of(
"route", Collections.unmodifiableList(rawRoutes),
"action", Collections.unmodifiableMap(rawActions)));
}
private static ImmutableMap<String, ?> generateWeightedTargetRawConfig(
@VisibleForTesting
static ImmutableMap<String, ?> convertToRawRoute(RouteMatch routeMatch, String actionName) {
ImmutableMap.Builder<String, Object> configRouteBuilder = new ImmutableMap.Builder<>();
PathMatcher pathMatcher = routeMatch.getPathMatch();
String path = pathMatcher.getPath();
String prefix = pathMatcher.getPrefix();
Pattern regex = pathMatcher.getRegEx();
if (path != null) {
configRouteBuilder.put("path", path);
}
if (prefix != null) {
configRouteBuilder.put("prefix", prefix);
}
if (regex != null) {
configRouteBuilder.put("regex", regex.pattern());
}
ImmutableList.Builder<Object> rawHeaderMatcherListBuilder = new ImmutableList.Builder<>();
List<HeaderMatcher> headerMatchers = routeMatch.getHeaderMatchers();
for (HeaderMatcher headerMatcher : headerMatchers) {
ImmutableMap.Builder<String, Object> rawHeaderMatcherBuilder = new ImmutableMap.Builder<>();
rawHeaderMatcherBuilder.put("name", headerMatcher.getName());
String exactMatch = headerMatcher.getExactMatch();
Pattern regexMatch = headerMatcher.getRegExMatch();
HeaderMatcher.Range rangeMatch = headerMatcher.getRangeMatch();
Boolean presentMatch = headerMatcher.getPresentMatch();
String prefixMatch = headerMatcher.getPrefixMatch();
String suffixMatch = headerMatcher.getSuffixMatch();
if (exactMatch != null) {
rawHeaderMatcherBuilder.put("exactMatch", exactMatch);
}
if (regexMatch != null) {
rawHeaderMatcherBuilder.put("regexMatch", regexMatch.pattern());
}
if (rangeMatch != null) {
rawHeaderMatcherBuilder
.put(
"rangeMatch",
ImmutableMap.of("start", rangeMatch.getStart(), "end", rangeMatch.getEnd()));
}
if (presentMatch != null) {
rawHeaderMatcherBuilder.put("presentMatch", presentMatch);
}
if (prefixMatch != null) {
rawHeaderMatcherBuilder.put("prefixMatch", prefixMatch);
}
if (suffixMatch != null) {
rawHeaderMatcherBuilder.put("suffixMatch", suffixMatch);
}
rawHeaderMatcherBuilder.put("invertMatch", headerMatcher.isInvertedMatch());
rawHeaderMatcherListBuilder.add(rawHeaderMatcherBuilder.build());
}
ImmutableList<?> rawHeaderMatchers = rawHeaderMatcherListBuilder.build();
if (!rawHeaderMatchers.isEmpty()) {
configRouteBuilder.put("headers", rawHeaderMatchers);
}
FractionMatcher matchFraction = routeMatch.getFractionMatch();
if (matchFraction != null) {
configRouteBuilder
.put(
"matchFraction",
ImmutableMap.of(
"numerator", matchFraction.getNumerator(),
"denominator", matchFraction.getDenominator()));
}
configRouteBuilder.put("action", actionName);
return configRouteBuilder.build();
}
@VisibleForTesting
static ImmutableMap<String, ?> generateWeightedTargetRawConfig(
List<ClusterWeight> clusterWeights) {
Map<String, Object> targets = new LinkedHashMap<>();
for (ClusterWeight clusterWeight : clusterWeights) {

View File

@ -28,13 +28,11 @@ import com.google.common.collect.ImmutableMap;
import io.grpc.ConnectivityState;
import io.grpc.InternalLogId;
import io.grpc.LoadBalancer;
import io.grpc.MethodDescriptor;
import io.grpc.Status;
import io.grpc.internal.ServiceConfigUtil.PolicySelection;
import io.grpc.util.ForwardingLoadBalancerHelper;
import io.grpc.util.GracefulSwitchLoadBalancer;
import io.grpc.xds.XdsLogger.XdsLogLevel;
import io.grpc.xds.XdsRoutingLoadBalancerProvider.MethodName;
import io.grpc.xds.XdsRoutingLoadBalancerProvider.Route;
import io.grpc.xds.XdsRoutingLoadBalancerProvider.XdsRoutingConfig;
import io.grpc.xds.XdsSubchannelPickers.ErrorPicker;
@ -131,15 +129,15 @@ final class XdsRoutingLoadBalancer extends LoadBalancer {
private void updateOverallBalancingState() {
ConnectivityState overallState = null;
// Use LinkedHashMap to preserve the order of routes.
Map<MethodName, SubchannelPicker> routePickers = new LinkedHashMap<>();
Map<RouteMatch, SubchannelPicker> routePickers = new LinkedHashMap<>();
for (Route route : routes) {
RouteHelper routeHelper = routeHelpers.get(route.actionName);
routePickers.put(route.methodName, routeHelper.currentPicker);
RouteHelper routeHelper = routeHelpers.get(route.getActionName());
routePickers.put(route.getRouteMatch(), routeHelper.currentPicker);
ConnectivityState routeState = routeHelper.currentState;
overallState = aggregateState(overallState, routeState);
}
if (overallState != null) {
SubchannelPicker picker = new PathMatchingSubchannelPicker(routePickers);
SubchannelPicker picker = new RouteMatchingSubchannelPicker(routePickers);
helper.updateBalancingState(overallState, picker);
}
}
@ -182,39 +180,18 @@ final class XdsRoutingLoadBalancer extends LoadBalancer {
}
}
private static final class PathMatchingSubchannelPicker extends SubchannelPicker {
private static final class RouteMatchingSubchannelPicker extends SubchannelPicker {
final Map<MethodName, SubchannelPicker> routePickers;
final Map<RouteMatch, SubchannelPicker> routePickers;
/**
* Constructs a picker that will match the path of PickSubchannelArgs with the given map.
* The order of the map entries matters. First match will be picked even if second match is an
* exact (service + method) path match.
*/
PathMatchingSubchannelPicker(Map<MethodName, SubchannelPicker> routePickers) {
RouteMatchingSubchannelPicker(Map<RouteMatch, SubchannelPicker> routePickers) {
this.routePickers = routePickers;
}
@Override
public PickResult pickSubchannel(PickSubchannelArgs args) {
for (MethodName methodName : routePickers.keySet()) {
if (match(args.getMethodDescriptor(), methodName)) {
return routePickers.get(methodName).pickSubchannel(args);
}
}
// At least the default route should match, otherwise there is a bug.
throw new IllegalStateException("PathMatchingSubchannelPicker: error in matching path");
}
boolean match(MethodDescriptor<?, ?> methodDescriptor, MethodName methodName) {
if (methodName.service.isEmpty() && methodName.method.isEmpty()) {
return true;
}
if (methodName.method.isEmpty()) {
return methodName.service.equals(methodDescriptor.getServiceName());
}
return (methodName.service + '/' + methodName.method)
.equals(methodDescriptor.getFullMethodName());
// TODO(chengyuanzhang): to be implemented.
return PickResult.withError(Status.INTERNAL.withDescription("routing picker unimplemented"));
}
}
}

View File

@ -20,6 +20,8 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.re2j.Pattern;
import com.google.re2j.PatternSyntaxException;
import io.grpc.Internal;
import io.grpc.LoadBalancer;
import io.grpc.LoadBalancer.Helper;
@ -31,13 +33,14 @@ import io.grpc.internal.JsonUtil;
import io.grpc.internal.ServiceConfigUtil;
import io.grpc.internal.ServiceConfigUtil.LbConfig;
import io.grpc.internal.ServiceConfigUtil.PolicySelection;
import io.grpc.xds.RouteMatch.FractionMatcher;
import io.grpc.xds.RouteMatch.HeaderMatcher;
import io.grpc.xds.RouteMatch.PathMatcher;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
/**
@ -97,72 +100,28 @@ public final class XdsRoutingLoadBalancerProvider extends LoadBalancerProvider {
return ConfigOrError.fromError(Status.INTERNAL.withDescription(
"No config for action " + name + " in xds_routing LB policy: " + rawConfig));
}
List<LbConfig> childConfigCandidates = ServiceConfigUtil.unwrapLoadBalancingConfigList(
JsonUtil.getListOfObjects(rawAction, "childPolicy"));
if (childConfigCandidates == null || childConfigCandidates.isEmpty()) {
return ConfigOrError.fromError(Status.INTERNAL.withDescription(
"No child policy for action " + name + " in xds_routing LB policy: "
+ rawConfig));
}
LoadBalancerRegistry lbRegistry =
this.lbRegistry == null ? LoadBalancerRegistry.getDefaultRegistry() : this.lbRegistry;
ConfigOrError selectedConfigOrError =
ServiceConfigUtil.selectLbPolicyFromList(childConfigCandidates, lbRegistry);
if (selectedConfigOrError.getError() != null) {
return selectedConfigOrError;
}
parsedActions.put(name, (PolicySelection) selectedConfigOrError.getConfig());
PolicySelection parsedAction =
parseAction(
rawAction,
this.lbRegistry == null
? LoadBalancerRegistry.getDefaultRegistry() : this.lbRegistry);
parsedActions.put(name, parsedAction);
}
List<Map<String, ?>> routes = JsonUtil.getListOfObjects(rawConfig, "route");
if (routes == null || routes.isEmpty()) {
List<Route> parsedRoutes = new ArrayList<>();
List<Map<String, ?>> rawRoutes = JsonUtil.getListOfObjects(rawConfig, "route");
if (rawRoutes == null || rawRoutes.isEmpty()) {
return ConfigOrError.fromError(Status.INTERNAL.withDescription(
"No routes provided for xds_routing LB policy: " + rawConfig));
}
List<Route> parsedRoutes = new ArrayList<>();
Set<MethodName> methodNames = new HashSet<>();
for (int i = 0; i < routes.size(); i++) {
Map<String, ?> route = routes.get(i);
String actionName = JsonUtil.getString(route, "action");
if (actionName == null) {
return ConfigOrError.fromError(Status.INTERNAL.withDescription(
"No action name provided for one of the routes in xds_routing LB policy: "
+ rawConfig));
}
if (!parsedActions.containsKey(actionName)) {
for (Map<String, ?> rawRoute: rawRoutes) {
Route route = parseRoute(rawRoute);
if (!parsedActions.containsKey(route.getActionName())) {
return ConfigOrError.fromError(Status.INTERNAL.withDescription(
"No action defined for route " + route + " in xds_routing LB policy: " + rawConfig));
}
Map<String, ?> methodName = JsonUtil.getObject(route, "methodName");
if (methodName == null) {
return ConfigOrError.fromError(Status.INTERNAL.withDescription(
"No method_name provided for one of the routes in xds_routing LB policy: "
+ rawConfig));
}
String service = JsonUtil.getString(methodName, "service");
String method = JsonUtil.getString(methodName, "method");
if (service == null || method == null) {
return ConfigOrError.fromError(Status.INTERNAL.withDescription(
"No service or method provided for one of the routes in xds_routing LB policy: "
+ rawConfig));
}
MethodName parseMethodName = new MethodName(service, method);
if (i == routes.size() - 1 && !parseMethodName.isDefault()) {
return ConfigOrError.fromError(Status.INTERNAL.withDescription(
"The last route in routes is not the default route in xds_routing LB policy: "
+ rawConfig));
}
if (methodNames.contains(parseMethodName)) {
return ConfigOrError.fromError(Status.INTERNAL.withDescription(
"Duplicate methodName found in routes in xds_routing LB policy: " + rawConfig));
}
methodNames.add(parseMethodName);
parsedRoutes.add(new Route(actionName, parseMethodName));
parsedRoutes.add(route);
}
return ConfigOrError.fromConfig(new XdsRoutingConfig(parsedRoutes, parsedActions));
} catch (RuntimeException e) {
return ConfigOrError.fromError(
@ -171,17 +130,146 @@ public final class XdsRoutingLoadBalancerProvider extends LoadBalancerProvider {
}
}
private static PolicySelection parseAction(
Map<String, ?> rawAction, LoadBalancerRegistry registry) {
List<LbConfig> childConfigCandidates = ServiceConfigUtil.unwrapLoadBalancingConfigList(
JsonUtil.getListOfObjects(rawAction, "childPolicy"));
if (childConfigCandidates == null || childConfigCandidates.isEmpty()) {
throw new RuntimeException("childPolicy not specified");
}
ConfigOrError selectedConfigOrError =
ServiceConfigUtil.selectLbPolicyFromList(childConfigCandidates, registry);
if (selectedConfigOrError.getError() != null) {
throw selectedConfigOrError.getError().asRuntimeException();
}
return (PolicySelection) selectedConfigOrError.getConfig();
}
private static Route parseRoute(Map<String, ?> rawRoute) {
try {
String pathExact = JsonUtil.getString(rawRoute, "path");
String pathPrefix = JsonUtil.getString(rawRoute, "prefix");
Pattern pathRegex = null;
String rawPathRegex = JsonUtil.getString(rawRoute, "regex");
if (rawPathRegex != null) {
try {
pathRegex = Pattern.compile(rawPathRegex);
} catch (PatternSyntaxException e) {
throw new RuntimeException(e);
}
}
if (!isOneOf(pathExact, pathPrefix, pathRegex)) {
throw new RuntimeException("must specify exactly one patch match type");
}
PathMatcher pathMatcher = new PathMatcher(pathExact, pathPrefix, pathRegex);
List<HeaderMatcher> headers = new ArrayList<>();
List<Map<String, ?>> rawHeaders = JsonUtil.getListOfObjects(rawRoute, "headers");
if (rawHeaders != null) {
for (Map<String, ?> rawHeader : rawHeaders) {
HeaderMatcher headerMatcher = parseHeaderMatcher(rawHeader);
headers.add(headerMatcher);
}
}
FractionMatcher matchFraction = null;
Map<String, ?> rawFraction = JsonUtil.getObject(rawRoute, "matchFraction");
if (rawFraction != null) {
matchFraction = parseFractionMatcher(rawFraction);
}
String actionName = JsonUtil.getString(rawRoute, "action");
if (actionName == null) {
throw new RuntimeException("action name not specified");
}
return new Route(new RouteMatch(pathMatcher, headers, matchFraction), actionName);
} catch (RuntimeException e) {
throw new RuntimeException("Failed to parse Route: " + e.getMessage());
}
}
private static HeaderMatcher parseHeaderMatcher(Map<String, ?> rawHeaderMatcher) {
try {
String name = JsonUtil.getString(rawHeaderMatcher, "name");
if (name == null) {
throw new RuntimeException("header name not specified");
}
String exactMatch = JsonUtil.getString(rawHeaderMatcher, "exactMatch");
Pattern regexMatch = null;
String rawRegex = JsonUtil.getString(rawHeaderMatcher, "regexMatch");
if (rawRegex != null) {
try {
regexMatch = Pattern.compile(rawRegex);
} catch (PatternSyntaxException e) {
throw new RuntimeException(e);
}
}
Map<String, ?> rawRangeMatch = JsonUtil.getObject(rawHeaderMatcher, "rangeMatch");
HeaderMatcher.Range rangeMatch =
rawRangeMatch == null ? null : parseHeaderRange(rawRangeMatch);
Boolean presentMatch = JsonUtil.getBoolean(rawHeaderMatcher, "presentMatch");
String prefixMatch = JsonUtil.getString(rawHeaderMatcher, "prefixMatch");
String suffixMatch = JsonUtil.getString(rawHeaderMatcher, "suffixMatch");
if (!isOneOf(exactMatch, regexMatch, rangeMatch, presentMatch, prefixMatch, suffixMatch)) {
throw new RuntimeException("must specify exactly one match type");
}
Boolean inverted = JsonUtil.getBoolean(rawHeaderMatcher, "invertMatch");
return new HeaderMatcher(
name, exactMatch, regexMatch, rangeMatch, presentMatch, prefixMatch, suffixMatch,
inverted == null ? false : inverted);
} catch (RuntimeException e) {
throw new RuntimeException("Failed to parse HeaderMatcher: " + e.getMessage());
}
}
private static boolean isOneOf(Object... objects) {
int count = 0;
for (Object o : objects) {
if (o != null) {
count++;
}
}
return count == 1;
}
private static HeaderMatcher.Range parseHeaderRange(Map<String, ?> rawRange) {
try {
Long start = JsonUtil.getNumberAsLong(rawRange, "start");
if (start == null) {
throw new RuntimeException("start not specified");
}
Long end = JsonUtil.getNumberAsLong(rawRange, "end");
if (end == null) {
throw new RuntimeException("end not specified");
}
return new HeaderMatcher.Range(start, end);
} catch (RuntimeException e) {
throw new RuntimeException("Failed to parse Range: " + e.getMessage());
}
}
private static FractionMatcher parseFractionMatcher(Map<String, ?> rawFraction) {
try {
Integer numerator = JsonUtil.getNumberAsInteger(rawFraction, "numerator");
if (numerator == null) {
throw new RuntimeException("numerator not specified");
}
Integer denominator = JsonUtil.getNumberAsInteger(rawFraction, "denominator");
if (denominator == null) {
throw new RuntimeException("denominator not specified");
}
return new FractionMatcher(numerator, denominator);
} catch (RuntimeException e) {
throw new RuntimeException("Failed to parse Fraction: " + e.getMessage());
}
}
static final class XdsRoutingConfig {
final List<Route> routes;
final Map<String, PolicySelection> actions;
/**
* Constructs a deeply parsed xds_routing config with the given non-empty list of routes, the
* action of each of which is provided by the given map of actions.
*/
@VisibleForTesting
XdsRoutingConfig(List<Route> routes, Map<String, PolicySelection> actions) {
private XdsRoutingConfig(List<Route> routes, Map<String, PolicySelection> actions) {
this.routes = ImmutableList.copyOf(routes);
this.actions = ImmutableMap.copyOf(actions);
}
@ -214,14 +302,25 @@ public final class XdsRoutingLoadBalancerProvider extends LoadBalancerProvider {
}
static final class Route {
private final RouteMatch routeMatch;
private final String actionName;
final String actionName;
final MethodName methodName;
@VisibleForTesting
Route(String actionName, MethodName methodName) {
Route(RouteMatch routeMatch, String actionName) {
this.routeMatch = routeMatch;
this.actionName = actionName;
this.methodName = methodName;
}
String getActionName() {
return actionName;
}
RouteMatch getRouteMatch() {
return routeMatch;
}
@Override
public int hashCode() {
return Objects.hash(routeMatch, actionName);
}
@Override
@ -232,63 +331,16 @@ public final class XdsRoutingLoadBalancerProvider extends LoadBalancerProvider {
if (o == null || getClass() != o.getClass()) {
return false;
}
Route route = (Route) o;
return Objects.equals(actionName, route.actionName)
&& Objects.equals(methodName, route.methodName);
}
@Override
public int hashCode() {
return Objects.hash(actionName, methodName);
Route that = (Route) o;
return Objects.equals(actionName, that.actionName)
&& Objects.equals(routeMatch, that.routeMatch);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("routeMatch", routeMatch)
.add("actionName", actionName)
.add("methodName", methodName)
.toString();
}
}
static final class MethodName {
final String service;
final String method;
@VisibleForTesting
MethodName(String service, String method) {
this.service = service;
this.method = method;
}
boolean isDefault() {
return service.isEmpty() && method.isEmpty();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MethodName that = (MethodName) o;
return Objects.equals(service, that.service)
&& Objects.equals(method, that.method);
}
@Override
public int hashCode() {
return Objects.hash(service, method);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("service", service)
.add("method", method)
.toString();
}
}

View File

@ -25,12 +25,13 @@ import com.google.re2j.Pattern;
import io.envoyproxy.envoy.api.v2.route.QueryParameterMatcher;
import io.envoyproxy.envoy.api.v2.route.RedirectAction;
import io.grpc.xds.EnvoyProtoData.ClusterWeight;
import io.grpc.xds.EnvoyProtoData.HeaderMatcher;
import io.grpc.xds.EnvoyProtoData.Locality;
import io.grpc.xds.EnvoyProtoData.Route;
import io.grpc.xds.EnvoyProtoData.RouteAction;
import io.grpc.xds.EnvoyProtoData.RouteMatch;
import io.grpc.xds.EnvoyProtoData.StructOrError;
import io.grpc.xds.RouteMatch.FractionMatcher;
import io.grpc.xds.RouteMatch.HeaderMatcher;
import io.grpc.xds.RouteMatch.PathMatcher;
import java.util.Arrays;
import java.util.Collections;
import javax.annotation.Nullable;
@ -103,8 +104,8 @@ public class EnvoyProtoDataTest {
assertThat(struct1.getStruct())
.isEqualTo(
new Route(
new RouteMatch(
null, "/service/method", null, null, Collections.<HeaderMatcher>emptyList()),
new RouteMatch(new PathMatcher("/service/method", null, null),
Collections.<HeaderMatcher>emptyList(), null),
new RouteAction("cluster-foo", null)));
io.envoyproxy.envoy.api.v2.route.Route unsupportedProto =
@ -188,25 +189,27 @@ public class EnvoyProtoDataTest {
// path_specifier = prefix
io.envoyproxy.envoy.api.v2.route.RouteMatch proto1 =
io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder().setPrefix("/").build();
StructOrError<RouteMatch> struct1 = RouteMatch.fromEnvoyProtoRouteMatch(proto1);
StructOrError<RouteMatch> struct1 = Route.convertEnvoyProtoRouteMatch(proto1);
assertThat(struct1.getErrorDetail()).isNull();
assertThat(struct1.getStruct()).isEqualTo(
new RouteMatch("/", null, null, null, Collections.<HeaderMatcher>emptyList()));
new RouteMatch(
new PathMatcher(null, "/", null), Collections.<HeaderMatcher>emptyList(), null));
// path_specifier = path
io.envoyproxy.envoy.api.v2.route.RouteMatch proto2 =
io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder().setPath("/service/method").build();
StructOrError<RouteMatch> struct2 = RouteMatch.fromEnvoyProtoRouteMatch(proto2);
StructOrError<RouteMatch> struct2 = Route.convertEnvoyProtoRouteMatch(proto2);
assertThat(struct2.getErrorDetail()).isNull();
assertThat(struct2.getStruct()).isEqualTo(
new RouteMatch(
null, "/service/method", null, null, Collections.<HeaderMatcher>emptyList()));
new PathMatcher("/service/method", null, null),
Collections.<HeaderMatcher>emptyList(), null));
// path_specifier = regex
@SuppressWarnings("deprecation")
io.envoyproxy.envoy.api.v2.route.RouteMatch proto3 =
io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder().setRegex("*").build();
StructOrError<RouteMatch> struct3 = RouteMatch.fromEnvoyProtoRouteMatch(proto3);
StructOrError<RouteMatch> struct3 = Route.convertEnvoyProtoRouteMatch(proto3);
assertThat(struct3.getErrorDetail()).isNotNull();
assertThat(struct3.getStruct()).isNull();
@ -215,18 +218,19 @@ public class EnvoyProtoDataTest {
io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder()
.setSafeRegex(
io.envoyproxy.envoy.type.matcher.RegexMatcher.newBuilder().setRegex(".")).build();
StructOrError<RouteMatch> struct4 = RouteMatch.fromEnvoyProtoRouteMatch(proto4);
StructOrError<RouteMatch> struct4 = Route.convertEnvoyProtoRouteMatch(proto4);
assertThat(struct4.getErrorDetail()).isNull();
assertThat(struct4.getStruct()).isEqualTo(
new RouteMatch(
null, null, Pattern.compile("."), null, Collections.<HeaderMatcher>emptyList()));
new PathMatcher(null, null, Pattern.compile(".")),
Collections.<HeaderMatcher>emptyList(), null));
// case_sensitive = false
io.envoyproxy.envoy.api.v2.route.RouteMatch proto5 =
io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder()
.setCaseSensitive(BoolValue.newBuilder().setValue(false))
.build();
StructOrError<RouteMatch> struct5 = RouteMatch.fromEnvoyProtoRouteMatch(proto5);
StructOrError<RouteMatch> struct5 = Route.convertEnvoyProtoRouteMatch(proto5);
assertThat(struct5.getErrorDetail()).isNotNull();
assertThat(struct5.getStruct()).isNull();
@ -235,13 +239,13 @@ public class EnvoyProtoDataTest {
io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder()
.addQueryParameters(QueryParameterMatcher.getDefaultInstance())
.build();
StructOrError<RouteMatch> struct6 = RouteMatch.fromEnvoyProtoRouteMatch(proto6);
StructOrError<RouteMatch> struct6 = Route.convertEnvoyProtoRouteMatch(proto6);
assertThat(struct6).isNull();
// path_specifier unset
io.envoyproxy.envoy.api.v2.route.RouteMatch unsetProto =
io.envoyproxy.envoy.api.v2.route.RouteMatch.getDefaultInstance();
StructOrError<RouteMatch> unsetStruct = RouteMatch.fromEnvoyProtoRouteMatch(unsetProto);
StructOrError<RouteMatch> unsetStruct = Route.convertEnvoyProtoRouteMatch(unsetProto);
assertThat(unsetStruct.getErrorDetail()).isNotNull();
assertThat(unsetStruct.getStruct()).isNull();
}
@ -260,14 +264,16 @@ public class EnvoyProtoDataTest {
.setName(":method")
.setExactMatch("PUT"))
.build();
StructOrError<RouteMatch> struct = RouteMatch.fromEnvoyProtoRouteMatch(proto);
StructOrError<RouteMatch> struct = Route.convertEnvoyProtoRouteMatch(proto);
assertThat(struct.getErrorDetail()).isNull();
assertThat(struct.getStruct())
.isEqualTo(
new RouteMatch("", null, null, null,
new RouteMatch(
new PathMatcher(null, "", null),
Arrays.asList(
new HeaderMatcher(":scheme", null, null, null, null, "http", null, false),
new HeaderMatcher(":method", "PUT", null, null, null, null, null, false))));
new HeaderMatcher(":method", "PUT", null, null, null, null, null, false)),
null));
}
@Test
@ -284,13 +290,13 @@ public class EnvoyProtoDataTest {
io.envoyproxy.envoy.type.FractionalPercent.DenominatorType
.HUNDRED)))
.build();
StructOrError<RouteMatch> struct = RouteMatch.fromEnvoyProtoRouteMatch(proto);
StructOrError<RouteMatch> struct = Route.convertEnvoyProtoRouteMatch(proto);
assertThat(struct.getErrorDetail()).isNull();
assertThat(struct.getStruct())
.isEqualTo(
new RouteMatch(
"", null, null, new RouteMatch.Fraction(30, 100),
Collections.<HeaderMatcher>emptyList()));
new PathMatcher(null, "", null), Collections.<HeaderMatcher>emptyList(),
new FractionMatcher(30, 100)));
}
@Test
@ -345,7 +351,7 @@ public class EnvoyProtoDataTest {
.setName(":method")
.setExactMatch("PUT")
.build();
StructOrError<HeaderMatcher> struct1 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto1);
StructOrError<HeaderMatcher> struct1 = Route.convertEnvoyProtoHeaderMatcher(proto1);
assertThat(struct1.getErrorDetail()).isNull();
assertThat(struct1.getStruct()).isEqualTo(
new HeaderMatcher(":method", "PUT", null, null, null, null, null, false));
@ -357,7 +363,7 @@ public class EnvoyProtoDataTest {
.setName(":method")
.setRegexMatch("*")
.build();
StructOrError<HeaderMatcher> struct2 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto2);
StructOrError<HeaderMatcher> struct2 = Route.convertEnvoyProtoHeaderMatcher(proto2);
assertThat(struct2.getErrorDetail()).isNotNull();
assertThat(struct2.getStruct()).isNull();
@ -368,7 +374,7 @@ public class EnvoyProtoDataTest {
.setSafeRegexMatch(
io.envoyproxy.envoy.type.matcher.RegexMatcher.newBuilder().setRegex("P*"))
.build();
StructOrError<HeaderMatcher> struct3 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto3);
StructOrError<HeaderMatcher> struct3 = Route.convertEnvoyProtoHeaderMatcher(proto3);
assertThat(struct3.getErrorDetail()).isNull();
assertThat(struct3.getStruct()).isEqualTo(
new HeaderMatcher(":method", null, Pattern.compile("P*"), null, null, null, null, false));
@ -380,7 +386,7 @@ public class EnvoyProtoDataTest {
.setRangeMatch(
io.envoyproxy.envoy.type.Int64Range.newBuilder().setStart(10L).setEnd(20L))
.build();
StructOrError<HeaderMatcher> struct4 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto4);
StructOrError<HeaderMatcher> struct4 = Route.convertEnvoyProtoHeaderMatcher(proto4);
assertThat(struct4.getErrorDetail()).isNull();
assertThat(struct4.getStruct()).isEqualTo(
new HeaderMatcher(
@ -392,7 +398,7 @@ public class EnvoyProtoDataTest {
.setName("user-agent")
.setPresentMatch(true)
.build();
StructOrError<HeaderMatcher> struct5 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto5);
StructOrError<HeaderMatcher> struct5 = Route.convertEnvoyProtoHeaderMatcher(proto5);
assertThat(struct5.getErrorDetail()).isNull();
assertThat(struct5.getStruct()).isEqualTo(
new HeaderMatcher("user-agent", null, null, null, true, null, null, false));
@ -403,7 +409,7 @@ public class EnvoyProtoDataTest {
.setName("authority")
.setPrefixMatch("service-foo")
.build();
StructOrError<HeaderMatcher> struct6 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto6);
StructOrError<HeaderMatcher> struct6 = Route.convertEnvoyProtoHeaderMatcher(proto6);
assertThat(struct6.getErrorDetail()).isNull();
assertThat(struct6.getStruct()).isEqualTo(
new HeaderMatcher("authority", null, null, null, null, "service-foo", null, false));
@ -414,7 +420,7 @@ public class EnvoyProtoDataTest {
.setName("authority")
.setSuffixMatch("googleapis.com")
.build();
StructOrError<HeaderMatcher> struct7 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto7);
StructOrError<HeaderMatcher> struct7 = Route.convertEnvoyProtoHeaderMatcher(proto7);
assertThat(struct7.getErrorDetail()).isNull();
assertThat(struct7.getStruct()).isEqualTo(
new HeaderMatcher(
@ -423,8 +429,7 @@ public class EnvoyProtoDataTest {
// header_match_specifier unset
io.envoyproxy.envoy.api.v2.route.HeaderMatcher unsetProto =
io.envoyproxy.envoy.api.v2.route.HeaderMatcher.getDefaultInstance();
StructOrError<HeaderMatcher> unsetStruct =
HeaderMatcher.fromEnvoyProtoHeaderMatcher(unsetProto);
StructOrError<HeaderMatcher> unsetStruct = Route.convertEnvoyProtoHeaderMatcher(unsetProto);
assertThat(unsetStruct.getErrorDetail()).isNotNull();
assertThat(unsetStruct.getStruct()).isNull();
}
@ -437,7 +442,7 @@ public class EnvoyProtoDataTest {
.setSafeRegexMatch(
io.envoyproxy.envoy.type.matcher.RegexMatcher.newBuilder().setRegex("["))
.build();
StructOrError<HeaderMatcher> struct = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto);
StructOrError<HeaderMatcher> struct = Route.convertEnvoyProtoHeaderMatcher(proto);
assertThat(struct.getErrorDetail()).isNotNull();
assertThat(struct.getStruct()).isNull();
}

View File

@ -725,14 +725,14 @@ public class XdsClientImplTest {
assertThat(routes.get(0)).isEqualTo(
new EnvoyProtoData.Route(
// path match with cluster route
new EnvoyProtoData.RouteMatch(
new io.grpc.xds.RouteMatch(
/* prefix= */ null,
/* path= */ "/service1/method1"),
new EnvoyProtoData.RouteAction("cl1.googleapis.com", null)));
assertThat(routes.get(1)).isEqualTo(
new EnvoyProtoData.Route(
// path match with weighted cluster route
new EnvoyProtoData.RouteMatch(
new io.grpc.xds.RouteMatch(
/* prefix= */ null,
/* path= */ "/service2/method2"),
new EnvoyProtoData.RouteAction(
@ -744,14 +744,14 @@ public class XdsClientImplTest {
assertThat(routes.get(2)).isEqualTo(
new EnvoyProtoData.Route(
// prefix match with cluster route
new EnvoyProtoData.RouteMatch(
new io.grpc.xds.RouteMatch(
/* prefix= */ "/service1/",
/* path= */ null),
new EnvoyProtoData.RouteAction("cl1.googleapis.com", null)));
assertThat(routes.get(3)).isEqualTo(
new EnvoyProtoData.Route(
// default match with cluster route
new EnvoyProtoData.RouteMatch(
new io.grpc.xds.RouteMatch(
/* prefix= */ "",
/* path= */ null),
new EnvoyProtoData.RouteAction(

View File

@ -0,0 +1,626 @@
/*
* Copyright 2020 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;
import static com.google.common.truth.Truth.assertThat;
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.XdsNameResolverTest.assertCdsPolicy;
import static io.grpc.xds.XdsNameResolverTest.assertWeightedTargetConfigClusterWeights;
import static io.grpc.xds.XdsNameResolverTest.assertWeightedTargetPolicy;
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;
import io.grpc.ChannelLogger;
import io.grpc.ManagedChannel;
import io.grpc.NameResolver;
import io.grpc.NameResolver.ConfigOrError;
import io.grpc.NameResolver.ResolutionResult;
import io.grpc.NameResolver.ServiceConfigParser;
import io.grpc.Status;
import io.grpc.Status.Code;
import io.grpc.SynchronizationContext;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.internal.BackoffPolicy;
import io.grpc.internal.FakeClock;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.ObjectPool;
import io.grpc.stub.StreamObserver;
import io.grpc.testing.GrpcCleanupRule;
import io.grpc.xds.Bootstrapper.ServerInfo;
import io.grpc.xds.XdsClient.XdsChannelFactory;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
/** Tests for {@link XdsNameResolver} with xDS service. */
@RunWith(JUnit4.class)
// TODO(creamsoup) use parsed service config
public class XdsNameResolverIntegrationTest {
private static final String AUTHORITY = "foo.googleapis.com:80";
private static final Node FAKE_BOOTSTRAP_NODE =
Node.newBuilder().setId("XdsNameResolverTest").build();
@Rule
public final MockitoRule mocks = MockitoJUnit.rule();
@Rule
public final GrpcCleanupRule cleanupRule = new GrpcCleanupRule();
private final SynchronizationContext syncContext = new SynchronizationContext(
new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
throw new AssertionError(e);
}
});
private final FakeClock fakeClock = new FakeClock();
private final Queue<StreamObserver<DiscoveryResponse>> responseObservers = new ArrayDeque<>();
private final ServiceConfigParser serviceConfigParser = new ServiceConfigParser() {
@Override
public ConfigOrError parseServiceConfig(Map<String, ?> rawServiceConfig) {
return ConfigOrError.fromConfig(rawServiceConfig);
}
};
private final NameResolver.Args args =
NameResolver.Args.newBuilder()
.setDefaultPort(8080)
.setProxyDetector(GrpcUtil.NOOP_PROXY_DETECTOR)
.setSynchronizationContext(syncContext)
.setServiceConfigParser(serviceConfigParser)
.setScheduledExecutorService(fakeClock.getScheduledExecutorService())
.setChannelLogger(mock(ChannelLogger.class))
.build();
ArgumentCaptor<ResolutionResult> resolutionResultCaptor = ArgumentCaptor.forClass(null);
@Mock
private BackoffPolicy.Provider backoffPolicyProvider;
@Mock
private NameResolver.Listener2 mockListener;
private XdsChannelFactory channelFactory;
private XdsNameResolver xdsNameResolver;
@Before
public void setUp() throws IOException {
final String serverName = InProcessServerBuilder.generateName();
AggregatedDiscoveryServiceImplBase serviceImpl = new AggregatedDiscoveryServiceImplBase() {
@Override
public StreamObserver<DiscoveryRequest> streamAggregatedResources(
final StreamObserver<DiscoveryResponse> responseObserver) {
responseObservers.offer(responseObserver);
@SuppressWarnings("unchecked")
StreamObserver<DiscoveryRequest> requestObserver = mock(StreamObserver.class);
return requestObserver;
}
};
cleanupRule.register(
InProcessServerBuilder
.forName(serverName)
.addService(serviceImpl)
.directExecutor()
.build()
.start());
final ManagedChannel channel =
cleanupRule.register(InProcessChannelBuilder.forName(serverName).directExecutor().build());
channelFactory = new XdsChannelFactory() {
@Override
ManagedChannel createChannel(List<ServerInfo> servers) {
assertThat(Iterables.getOnlyElement(servers).getServerUri()).isEqualTo(serverName);
return channel;
}
};
Bootstrapper bootstrapper = new Bootstrapper() {
@Override
public BootstrapInfo readBootstrap() {
List<ServerInfo> serverList =
ImmutableList.of(
new ServerInfo(serverName,
ImmutableList.<ChannelCreds>of()));
return new BootstrapInfo(serverList, FAKE_BOOTSTRAP_NODE);
}
};
xdsNameResolver =
new XdsNameResolver(
AUTHORITY,
args,
backoffPolicyProvider,
fakeClock.getStopwatchSupplier(),
channelFactory,
bootstrapper);
assertThat(responseObservers).isEmpty();
}
@After
public void tearDown() {
xdsNameResolver.shutdown();
XdsClientImpl.enableExperimentalRouting = false;
}
@Test
public void resolve_bootstrapProvidesNoTrafficDirectorInfo() {
Bootstrapper bootstrapper = new Bootstrapper() {
@Override
public BootstrapInfo readBootstrap() {
return new BootstrapInfo(ImmutableList.<ServerInfo>of(), FAKE_BOOTSTRAP_NODE);
}
};
XdsNameResolver resolver =
new XdsNameResolver(
AUTHORITY,
args,
backoffPolicyProvider,
fakeClock.getStopwatchSupplier(),
channelFactory,
bootstrapper);
resolver.start(mockListener);
ArgumentCaptor<Status> statusCaptor = ArgumentCaptor.forClass(null);
verify(mockListener).onError(statusCaptor.capture());
assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNAVAILABLE);
assertThat(statusCaptor.getValue().getDescription())
.isEqualTo("No management server provided by bootstrap");
}
@Test
public void resolve_failToBootstrap() {
Bootstrapper bootstrapper = new Bootstrapper() {
@Override
public BootstrapInfo readBootstrap() throws IOException {
throw new IOException("Fail to read bootstrap file");
}
};
XdsNameResolver resolver =
new XdsNameResolver(
AUTHORITY,
args,
backoffPolicyProvider,
fakeClock.getStopwatchSupplier(),
channelFactory,
bootstrapper);
resolver.start(mockListener);
ArgumentCaptor<Status> errorCaptor = ArgumentCaptor.forClass(null);
verify(mockListener).onError(errorCaptor.capture());
Status error = errorCaptor.getValue();
assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE);
assertThat(error.getDescription()).isEqualTo("Failed to bootstrap");
assertThat(error.getCause()).hasMessageThat().isEqualTo("Fail to read bootstrap file");
}
@Test
public void resolve_passXdsClientPoolInResult() {
xdsNameResolver.start(mockListener);
assertThat(responseObservers).hasSize(1);
StreamObserver<DiscoveryResponse> responseObserver = responseObservers.poll();
// Simulate receiving an LDS response that contains cluster resolution directly in-line.
String clusterName = "cluster-foo.googleapis.com";
responseObserver.onNext(
buildLdsResponseForCluster("0", AUTHORITY, clusterName, "0000"));
verify(mockListener).onResult(resolutionResultCaptor.capture());
ResolutionResult result = resolutionResultCaptor.getValue();
ObjectPool<XdsClient> xdsClientPool = result.getAttributes().get(XdsAttributes.XDS_CLIENT_POOL);
assertThat(xdsClientPool).isNotNull();
}
@SuppressWarnings("unchecked")
@Test
public void resolve_ResourceNotFound() {
xdsNameResolver.start(mockListener);
assertThat(responseObservers).hasSize(1);
StreamObserver<DiscoveryResponse> responseObserver = responseObservers.poll();
// Simulate receiving an LDS response that does not contain requested resource.
String clusterName = "cluster-bar.googleapis.com";
responseObserver.onNext(
buildLdsResponseForCluster("0", "bar.googleapis.com", clusterName, "0000"));
fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS);
verify(mockListener).onResult(resolutionResultCaptor.capture());
ResolutionResult result = resolutionResultCaptor.getValue();
assertThat(result.getAddresses()).isEmpty();
assertThat((Map<String, ?>) result.getServiceConfig().getConfig()).isEmpty();
}
@Test
@SuppressWarnings("unchecked")
public void resolve_resourceUpdated() {
xdsNameResolver.start(mockListener);
assertThat(responseObservers).hasSize(1);
StreamObserver<DiscoveryResponse> responseObserver = responseObservers.poll();
// Simulate receiving an LDS response that contains cluster resolution directly in-line.
responseObserver.onNext(
buildLdsResponseForCluster("0", AUTHORITY, "cluster-foo.googleapis.com", "0000"));
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("cds_experimental");
Map<String, ?> rawConfigValues = (Map<String, ?>) lbConfig.get("cds_experimental");
assertThat(rawConfigValues).containsExactly("cluster", "cluster-foo.googleapis.com");
// 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 cluster resolution for "foo.googleapis.com".
responseObserver.onNext(
buildRdsResponseForCluster("0", routeConfigName, AUTHORITY,
"cluster-blade.googleapis.com", "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("cds_experimental");
rawConfigValues = (Map<String, ?>) lbConfig.get("cds_experimental");
assertThat(rawConfigValues).containsExactly("cluster", "cluster-blade.googleapis.com");
}
@SuppressWarnings("unchecked")
@Test
public void resolve_cdsLoadBalancing() {
xdsNameResolver.start(mockListener);
assertThat(responseObservers).hasSize(1);
StreamObserver<DiscoveryResponse> responseObserver = responseObservers.poll();
// Simulate receiving an LDS response that contains cluster resolution directly in-line.
String clusterName = "cluster-foo.googleapis.com";
responseObserver.onNext(
buildLdsResponseForCluster("0", AUTHORITY, clusterName, "0000"));
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("cds_experimental");
Map<String, ?> rawConfigValues = (Map<String, ?>) lbConfig.get("cds_experimental");
assertThat(rawConfigValues).containsExactly("cluster", clusterName);
}
@Test
@SuppressWarnings("unchecked")
public void resolve_xdsRoutingLoadBalancing() {
XdsClientImpl.enableExperimentalRouting = 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(RouteMatch.newBuilder().setPath("/fooSvc/hello"))
.setRoute(buildClusterRoute("cluster-hello.googleapis.com"))
.build(),
// prefix match, routed to cluster
Route.newBuilder()
.setMatch(RouteMatch.newBuilder().setPrefix("/fooSvc/"))
.setRoute(buildClusterRoute("cluster-foo.googleapis.com"))
.build(),
// path match, routed to weighted clusters
Route.newBuilder()
.setMatch(RouteMatch.newBuilder().setPath("/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(RouteMatch.newBuilder().setPrefix("/barSvc/"))
.setRoute(
buildWeightedClusterRoute(
ImmutableMap.of(
"cluster-bar.googleapis.com", 30, "cluster-bar2.googleapis.com", 70)))
.build(),
// default with prefix = "/", routed to cluster
Route.newBuilder()
.setMatch(RouteMatch.newBuilder().setPrefix("/"))
.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"));
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(actions).hasSize(4);
assertThat(routes).hasSize(5);
Map<String, ?> route0 = routes.get(0);
assertThat(route0.keySet()).containsExactly("path", "action");
assertThat((String) route0.get("path")).isEqualTo("/fooSvc/hello");
assertCdsPolicy(actions.get(route0.get("action")), "cluster-hello.googleapis.com");
Map<String, ?> route1 = routes.get(1);
assertThat(route1.keySet()).containsExactly("prefix", "action");
assertThat((String) route1.get("prefix")).isEqualTo("/fooSvc/");
assertCdsPolicy(actions.get(route1.get("action")), "cluster-foo.googleapis.com");
Map<String, ?> route2 = routes.get(2);
assertThat(route2.keySet()).containsExactly("path", "action");
assertThat((String) route2.get("path")).isEqualTo("/barSvc/hello");
assertWeightedTargetPolicy(
actions.get(route2.get("action")),
ImmutableMap.of(
"cluster-hello.googleapis.com", 40, "cluster-hello2.googleapis.com", 60));
Map<String, ?> route3 = routes.get(3);
assertThat(route3.keySet()).containsExactly("prefix", "action");
assertThat((String) route3.get("prefix")).isEqualTo("/barSvc/");
assertWeightedTargetPolicy(
actions.get(route3.get("action")),
ImmutableMap.of(
"cluster-bar.googleapis.com", 30, "cluster-bar2.googleapis.com", 70));
Map<String, ?> route4 = routes.get(4);
assertThat(route4.keySet()).containsExactly("prefix", "action");
assertThat((String) route4.get("prefix")).isEqualTo("/");
assertCdsPolicy(actions.get(route4.get("action")), "cluster-hello.googleapis.com");
}
@SuppressWarnings("unchecked")
@Test
public void resolve_weightedTargetLoadBalancing() {
XdsClientImpl.enableExperimentalRouting = true;
xdsNameResolver.start(mockListener);
assertThat(responseObservers).hasSize(1);
StreamObserver<DiscoveryResponse> responseObserver = responseObservers.poll();
// 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()
.setMatch(RouteMatch.newBuilder().setPrefix(""))
.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).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("weighted_target_experimental");
Map<String, ?> rawConfigValues = (Map<String, ?>) lbConfig.get("weighted_target_experimental");
assertWeightedTargetConfigClusterWeights(
rawConfigValues,
ImmutableMap.of(
"cluster-foo.googleapis.com", 20, "cluster-bar.googleapis.com", 80));
}
@Test
@SuppressWarnings("unchecked")
public void resolve_resourceNewlyAdded() {
xdsNameResolver.start(mockListener);
assertThat(responseObservers).hasSize(1);
StreamObserver<DiscoveryResponse> responseObserver = responseObservers.poll();
// Simulate receiving an LDS response that does not contain requested resource.
responseObserver.onNext(
buildLdsResponseForCluster("0", "bar.googleapis.com",
"cluster-bar.googleapis.com", "0000"));
fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS);
verify(mockListener).onResult(resolutionResultCaptor.capture());
ResolutionResult result = resolutionResultCaptor.getValue();
assertThat(result.getAddresses()).isEmpty();
// Simulate receiving another LDS response that contains cluster resolution directly in-line.
responseObserver.onNext(
buildLdsResponseForCluster("1", AUTHORITY, "cluster-foo.googleapis.com",
"0001"));
verify(mockListener, times(2)).onResult(resolutionResultCaptor.capture());
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("cds_experimental");
Map<String, ?> rawConfigValues = (Map<String, ?>) lbConfig.get("cds_experimental");
assertThat(rawConfigValues).containsExactly("cluster", "cluster-foo.googleapis.com");
}
/**
* Builds an LDS DiscoveryResponse containing the mapping of given host to
* the given cluster name directly in-line. Clients receiving this response is
* able to resolve cluster name for the given host immediately.
*/
private static DiscoveryResponse buildLdsResponseForCluster(
String versionInfo, String host, String clusterName, String nonce) {
List<Any> listeners = ImmutableList.of(
Any.pack(buildListener(host, // target Listener resource
Any.pack(
HttpConnectionManager.newBuilder()
.setRouteConfig(
buildRouteConfiguration("route-foo.googleapis.com", // doesn't matter
ImmutableList.of(
buildVirtualHost(
ImmutableList.of(host), // exact match
clusterName))))
.build()))));
return buildDiscoveryResponse(versionInfo, listeners, XdsClientImpl.ADS_TYPE_URL_LDS, nonce);
}
/**
* Builds an LDS DiscoveryResponse containing the mapping of given host to
* the given RDS resource name. Clients receiving this response is able to
* send an RDS request for resolving the cluster name for the given host.
*/
private static DiscoveryResponse buildLdsResponseForRdsResource(
String versionInfo, String host, String routeConfigName, String nonce) {
Rds rdsConfig =
Rds.newBuilder()
// Must set to use ADS.
.setConfigSource(
ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance()))
.setRouteConfigName(routeConfigName)
.build();
List<Any> listeners = ImmutableList.of(
Any.pack(
buildListener(
host, Any.pack(HttpConnectionManager.newBuilder().setRds(rdsConfig).build()))));
return buildDiscoveryResponse(versionInfo, listeners, XdsClientImpl.ADS_TYPE_URL_LDS, nonce);
}
/**
* Builds an RDS DiscoveryResponse containing route configuration with the given name and a
* virtual host that matches the given host to the given cluster name.
*/
private static DiscoveryResponse buildRdsResponseForCluster(
String versionInfo,
String routeConfigName,
String host,
String clusterName,
String nonce) {
List<Any> routeConfigs = ImmutableList.of(
Any.pack(
buildRouteConfiguration(
routeConfigName,
ImmutableList.of(
buildVirtualHost(ImmutableList.of(host), clusterName)))));
return buildDiscoveryResponse(versionInfo, routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, nonce);
}
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.of(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();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019 The gRPC Authors
* Copyright 2020 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.
@ -17,571 +17,221 @@
package io.grpc.xds;
import static com.google.common.truth.Truth.assertThat;
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 static io.grpc.xds.XdsLbPolicies.XDS_ROUTING_POLICY_NAME;
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;
import io.grpc.ChannelLogger;
import io.grpc.ManagedChannel;
import io.grpc.NameResolver;
import io.grpc.NameResolver.ConfigOrError;
import io.grpc.NameResolver.ResolutionResult;
import io.grpc.NameResolver.ServiceConfigParser;
import io.grpc.Status;
import io.grpc.Status.Code;
import io.grpc.SynchronizationContext;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.internal.BackoffPolicy;
import io.grpc.internal.FakeClock;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.ObjectPool;
import io.grpc.stub.StreamObserver;
import io.grpc.testing.GrpcCleanupRule;
import io.grpc.xds.Bootstrapper.ServerInfo;
import io.grpc.xds.XdsClient.XdsChannelFactory;
import com.google.re2j.Pattern;
import io.grpc.internal.JsonParser;
import io.grpc.xds.EnvoyProtoData.ClusterWeight;
import io.grpc.xds.EnvoyProtoData.Route;
import io.grpc.xds.EnvoyProtoData.RouteAction;
import io.grpc.xds.RouteMatch.FractionMatcher;
import io.grpc.xds.RouteMatch.HeaderMatcher;
import io.grpc.xds.RouteMatch.PathMatcher;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
/** Unit tests for {@link XdsNameResolver}. */
@RunWith(JUnit4.class)
// TODO(creamsoup) use parsed service config
public class XdsNameResolverTest {
private static final String AUTHORITY = "foo.googleapis.com:80";
private static final Node FAKE_BOOTSTRAP_NODE =
Node.newBuilder().setId("XdsNameResolverTest").build();
@Rule
public final MockitoRule mocks = MockitoJUnit.rule();
@Rule
public final GrpcCleanupRule cleanupRule = new GrpcCleanupRule();
private final SynchronizationContext syncContext = new SynchronizationContext(
new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
throw new AssertionError(e);
}
});
private final FakeClock fakeClock = new FakeClock();
private final Queue<StreamObserver<DiscoveryResponse>> responseObservers = new ArrayDeque<>();
private final ServiceConfigParser serviceConfigParser = new ServiceConfigParser() {
@Override
public ConfigOrError parseServiceConfig(Map<String, ?> rawServiceConfig) {
return ConfigOrError.fromConfig(rawServiceConfig);
}
};
private final NameResolver.Args args =
NameResolver.Args.newBuilder()
.setDefaultPort(8080)
.setProxyDetector(GrpcUtil.NOOP_PROXY_DETECTOR)
.setSynchronizationContext(syncContext)
.setServiceConfigParser(serviceConfigParser)
.setScheduledExecutorService(fakeClock.getScheduledExecutorService())
.setChannelLogger(mock(ChannelLogger.class))
.build();
@Mock
private BackoffPolicy.Provider backoffPolicyProvider;
@Mock
private NameResolver.Listener2 mockListener;
private XdsChannelFactory channelFactory;
private XdsNameResolver xdsNameResolver;
@Before
public void setUp() throws IOException {
final String serverName = InProcessServerBuilder.generateName();
AggregatedDiscoveryServiceImplBase serviceImpl = new AggregatedDiscoveryServiceImplBase() {
@Override
public StreamObserver<DiscoveryRequest> streamAggregatedResources(
final StreamObserver<DiscoveryResponse> responseObserver) {
responseObservers.offer(responseObserver);
@SuppressWarnings("unchecked")
StreamObserver<DiscoveryRequest> requestObserver = mock(StreamObserver.class);
return requestObserver;
}
};
cleanupRule.register(
InProcessServerBuilder
.forName(serverName)
.addService(serviceImpl)
.directExecutor()
.build()
.start());
final ManagedChannel channel =
cleanupRule.register(InProcessChannelBuilder.forName(serverName).directExecutor().build());
channelFactory = new XdsChannelFactory() {
@Override
ManagedChannel createChannel(List<ServerInfo> servers) {
assertThat(Iterables.getOnlyElement(servers).getServerUri()).isEqualTo(serverName);
return channel;
}
};
Bootstrapper bootstrapper = new Bootstrapper() {
@Override
public BootstrapInfo readBootstrap() {
List<ServerInfo> serverList =
ImmutableList.of(
new ServerInfo(serverName,
ImmutableList.<ChannelCreds>of()));
return new BootstrapInfo(serverList, FAKE_BOOTSTRAP_NODE);
}
};
xdsNameResolver =
new XdsNameResolver(
AUTHORITY,
args,
backoffPolicyProvider,
fakeClock.getStopwatchSupplier(),
channelFactory,
bootstrapper);
assertThat(responseObservers).isEmpty();
}
@After
public void tearDown() {
xdsNameResolver.shutdown();
XdsClientImpl.enableExperimentalRouting = false;
}
@Test
public void resolve_bootstrapProvidesNoTrafficDirectorInfo() {
Bootstrapper bootstrapper = new Bootstrapper() {
@Override
public BootstrapInfo readBootstrap() {
return new BootstrapInfo(ImmutableList.<ServerInfo>of(), FAKE_BOOTSTRAP_NODE);
}
};
XdsNameResolver resolver =
new XdsNameResolver(
AUTHORITY,
args,
backoffPolicyProvider,
fakeClock.getStopwatchSupplier(),
channelFactory,
bootstrapper);
resolver.start(mockListener);
ArgumentCaptor<Status> statusCaptor = ArgumentCaptor.forClass(null);
verify(mockListener).onError(statusCaptor.capture());
assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNAVAILABLE);
assertThat(statusCaptor.getValue().getDescription())
.isEqualTo("No management server provided by bootstrap");
}
@Test
public void resolve_failToBootstrap() {
Bootstrapper bootstrapper = new Bootstrapper() {
@Override
public BootstrapInfo readBootstrap() throws IOException {
throw new IOException("Fail to read bootstrap file");
}
};
XdsNameResolver resolver =
new XdsNameResolver(
AUTHORITY,
args,
backoffPolicyProvider,
fakeClock.getStopwatchSupplier(),
channelFactory,
bootstrapper);
resolver.start(mockListener);
ArgumentCaptor<Status> errorCaptor = ArgumentCaptor.forClass(null);
verify(mockListener).onError(errorCaptor.capture());
Status error = errorCaptor.getValue();
assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE);
assertThat(error.getDescription()).isEqualTo("Failed to bootstrap");
assertThat(error.getCause()).hasMessageThat().isEqualTo("Fail to read bootstrap file");
}
@Test
public void resolve_passXdsClientPoolInResult() {
xdsNameResolver.start(mockListener);
assertThat(responseObservers).hasSize(1);
StreamObserver<DiscoveryResponse> responseObserver = responseObservers.poll();
// Simulate receiving an LDS response that contains cluster resolution directly in-line.
String clusterName = "cluster-foo.googleapis.com";
responseObserver.onNext(
buildLdsResponseForCluster("0", AUTHORITY, clusterName, "0000"));
ArgumentCaptor<ResolutionResult> resolutionResultCaptor = ArgumentCaptor.forClass(null);
verify(mockListener).onResult(resolutionResultCaptor.capture());
ResolutionResult result = resolutionResultCaptor.getValue();
ObjectPool<XdsClient> xdsClientPool = result.getAttributes().get(XdsAttributes.XDS_CLIENT_POOL);
assertThat(xdsClientPool).isNotNull();
}
@Test
public void resolve_foundResource() {
xdsNameResolver.start(mockListener);
assertThat(responseObservers).hasSize(1);
StreamObserver<DiscoveryResponse> responseObserver = responseObservers.poll();
// Simulate receiving an LDS response that contains cluster resolution directly in-line.
String clusterName = "cluster-foo.googleapis.com";
responseObserver.onNext(
buildLdsResponseForCluster("0", AUTHORITY, clusterName, "0000"));
ArgumentCaptor<ResolutionResult> resolutionResultCaptor = ArgumentCaptor.forClass(null);
verify(mockListener).onResult(resolutionResultCaptor.capture());
ResolutionResult result = resolutionResultCaptor.getValue();
assertThat(result.getAddresses()).isEmpty();
public void generateWeightedTargetRawConfig() throws IOException {
List<ClusterWeight> clusterWeights =
Arrays.asList(
new ClusterWeight("cluster-foo", 30), new ClusterWeight("cluster-bar", 50));
Map<String, ?> config = XdsNameResolver.generateWeightedTargetRawConfig(clusterWeights);
String expectedJson = "{\n"
+ " \"weighted_target_experimental\": {\n"
+ " \"targets\": {\n"
+ " \"cluster-foo\": {\n"
+ " \"weight\": 30,\n"
+ " \"childPolicy\": [{\n"
+ " \"cds_experimental\": {\n"
+ " \"cluster\": \"cluster-foo\"\n"
+ " }\n"
+ " }]\n"
+ " },\n"
+ " \"cluster-bar\": {\n"
+ " \"weight\": 50,\n"
+ " \"childPolicy\": [{\n"
+ " \"cds_experimental\": {\n"
+ " \"cluster\": \"cluster-bar\"\n"
+ " }\n"
+ " }]\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ "}";
assertThat(config).isEqualTo(JsonParser.parse(expectedJson));
}
@SuppressWarnings("unchecked")
@Test
public void resolve_ResourceNotFound() {
xdsNameResolver.start(mockListener);
assertThat(responseObservers).hasSize(1);
StreamObserver<DiscoveryResponse> responseObserver = responseObservers.poll();
public void generateXdsRoutingRawConfig() {
Route r1 =
new Route(
new RouteMatch(
new PathMatcher(null, "", null), Collections.<HeaderMatcher>emptyList(),
new FractionMatcher(10, 20)),
new RouteAction("cluster-foo", null));
Route r2 =
new Route(
new RouteMatch(
new PathMatcher("/service/method", null, null),
Arrays.asList(
new HeaderMatcher(":scheme", "https", null, null, null, null, null, false)),
null),
new RouteAction(
null,
Arrays.asList(
new ClusterWeight("cluster-foo", 20),
new ClusterWeight("cluster-bar", 20))));
// Simulate receiving an LDS response that does not contain requested resource.
String clusterName = "cluster-bar.googleapis.com";
responseObserver.onNext(
buildLdsResponseForCluster("0", "bar.googleapis.com", clusterName, "0000"));
Map<String, ?> config =
XdsNameResolver.generateXdsRoutingRawConfig(Arrays.asList(r1, r2));
assertThat(config.keySet()).containsExactly("xds_routing_experimental");
Map<String, ?> content = (Map<String, ?>) config.get(XDS_ROUTING_POLICY_NAME);
assertThat(content.keySet()).containsExactly("action", "route");
Map<String, Map<String, ?>> actions = (Map<String, Map<String, ?>>) content.get("action");
List<Map<String, ?>> routes = (List<Map<String, ?>>) content.get("route");
assertThat(actions).hasSize(2);
assertThat(routes).hasSize(2);
fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS);
ArgumentCaptor<ResolutionResult> resolutionResultCaptor = ArgumentCaptor.forClass(null);
verify(mockListener).onResult(resolutionResultCaptor.capture());
ResolutionResult result = resolutionResultCaptor.getValue();
assertThat(result.getAddresses()).isEmpty();
assertThat((Map<String, ?>) result.getServiceConfig().getConfig()).isEmpty();
Map<String, ?> route0 = routes.get(0);
assertThat(route0.keySet()).containsExactly("prefix", "matchFraction", "action");
assertThat((String) route0.get("prefix")).isEqualTo("");
assertThat((Map<String, ?>) route0.get("matchFraction"))
.containsExactly("numerator", 10, "denominator", 20);
assertCdsPolicy(actions.get(route0.get("action")), "cluster-foo");
Map<String, ?> route1 = routes.get(1);
assertThat(route1.keySet()).containsExactly("path", "headers", "action");
assertThat((String) route1.get("path")).isEqualTo("/service/method");
Map<String, ?> header = Iterables.getOnlyElement((List<Map<String, ?>>) route1.get("headers"));
assertThat(header)
.containsExactly("name", ":scheme", "exactMatch", "https", "invertMatch", false);
assertWeightedTargetPolicy(
actions.get(route1.get("action")),
ImmutableMap.of(
"cluster-foo", 20, "cluster-bar", 20));
}
@Test
@SuppressWarnings("unchecked")
public void resolve_resourceUpdated() {
xdsNameResolver.start(mockListener);
assertThat(responseObservers).hasSize(1);
StreamObserver<DiscoveryResponse> responseObserver = responseObservers.poll();
@Test
public void generateXdsRoutingRawConfig_allowDuplicateMatchers() {
Route route =
new Route(
new RouteMatch(
new PathMatcher("/service/method", null, null),
Collections.<HeaderMatcher>emptyList(), null),
new RouteAction("cluster-foo", null));
// Simulate receiving an LDS response that contains cluster resolution directly in-line.
responseObserver.onNext(
buildLdsResponseForCluster("0", AUTHORITY, "cluster-foo.googleapis.com", "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("cds_experimental");
Map<String, ?> rawConfigValues = (Map<String, ?>) lbConfig.get("cds_experimental");
assertThat(rawConfigValues).containsExactly("cluster", "cluster-foo.googleapis.com");
// 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 cluster resolution for "foo.googleapis.com".
responseObserver.onNext(
buildRdsResponseForCluster("0", routeConfigName, AUTHORITY,
"cluster-blade.googleapis.com", "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("cds_experimental");
rawConfigValues = (Map<String, ?>) lbConfig.get("cds_experimental");
assertThat(rawConfigValues).containsExactly("cluster", "cluster-blade.googleapis.com");
Map<String, ?> config =
XdsNameResolver.generateXdsRoutingRawConfig(Arrays.asList(route, route));
assertThat(config.keySet()).containsExactly(XDS_ROUTING_POLICY_NAME);
Map<String, ?> content = (Map<String, ?>) config.get(XDS_ROUTING_POLICY_NAME);
assertThat(content.keySet()).containsExactly("action", "route");
Map<String, ?> actions = (Map<String, ?>) content.get("action");
List<?> routes = (List<?>) content.get("route");
assertThat(actions).hasSize(1);
assertThat(routes).hasSize(2);
assertThat(routes.get(0)).isEqualTo(routes.get(1));
}
@Test
@SuppressWarnings("unchecked")
public void resolve_resourceUpdated_multipleRoutes() {
XdsClientImpl.enableExperimentalRouting = 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(buildPathExactMatch("fooSvc", "hello"))
.setRoute(buildClusterRoute("cluster-hello.googleapis.com"))
.build(),
// prefix match, routed to cluster
Route.newBuilder()
.setMatch(buildPathPrefixMatch("fooSvc"))
.setRoute(buildClusterRoute("cluster-foo.googleapis.com"))
.build(),
// path match, routed to weighted clusters
Route.newBuilder()
.setMatch(buildPathExactMatch("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(buildPathPrefixMatch("barSvc"))
.setRoute(
buildWeightedClusterRoute(
ImmutableMap.of(
"cluster-bar.googleapis.com", 30, "cluster-bar2.googleapis.com", 70)))
.build(),
// default with prefix = "/", routed to cluster
Route.newBuilder()
.setMatch(RouteMatch.newBuilder().setPrefix("/"))
.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()
.setMatch(RouteMatch.newBuilder().setPrefix(""))
.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));
}
@Test
@SuppressWarnings("unchecked")
public void resolve_resourceUpdated_allowDuplicateMatchers() {
XdsClientImpl.enableExperimentalRouting = true;
xdsNameResolver.start(mockListener);
assertThat(responseObservers).hasSize(1);
StreamObserver<DiscoveryResponse> responseObserver = responseObservers.poll();
// Simulate receiving another LDS response that tells client to do RDS.
String routeConfigName = "route-foo.googleapis.com";
responseObserver.onNext(
buildLdsResponseForRdsResource("1", AUTHORITY, routeConfigName, "0001"));
public void convertToRawRoute() throws IOException {
RouteMatch routeMatch1 =
new RouteMatch(
new PathMatcher("/service/method", null, null),
Collections.<HeaderMatcher>emptyList(), null);
String expectedJson1 = "{\n"
+ " \"path\": \"/service/method\",\n"
+ " \"action\": \"action_foo\""
+ "}";
assertThat(XdsNameResolver.convertToRawRoute(routeMatch1, "action_foo"))
.isEqualTo(JsonParser.parse(expectedJson1));
// Client sent an RDS request for resource "route-foo.googleapis.com" (Omitted in this test).
List<Route> protoRoutes =
ImmutableList.of(
// path match, routed to cluster
Route.newBuilder()
.setMatch(buildPathExactMatch("fooSvc", "hello"))
.setRoute(buildClusterRoute("cluster-hello.googleapis.com"))
.build(),
// prefix match, routed to cluster
Route.newBuilder()
.setMatch(buildPathPrefixMatch("fooSvc"))
.setRoute(buildClusterRoute("cluster-foo.googleapis.com"))
.build(),
// duplicate path match, routed to weighted clusters
Route.newBuilder()
.setMatch(buildPathExactMatch("fooSvc", "hello"))
.setRoute(buildWeightedClusterRoute(ImmutableMap.of(
"cluster-hello.googleapis.com", 40, "cluster-hello2.googleapis.com", 60)))
.build(),
// duplicate prefix match, routed to weighted clusters
Route.newBuilder()
.setMatch(buildPathPrefixMatch("fooSvc"))
.setRoute(
buildWeightedClusterRoute(
ImmutableMap.of(
"cluster-bar.googleapis.com", 30, "cluster-bar2.googleapis.com", 70)))
.build(),
// default, routed to cluster
Route.newBuilder()
.setMatch(RouteMatch.newBuilder().setPrefix(""))
.setRoute(buildClusterRoute("cluster-hello.googleapis.com"))
.build());
List<Any> routeConfigs = ImmutableList.of(
Any.pack(
buildRouteConfiguration(
routeConfigName,
ImmutableList.of(buildVirtualHostForRoutes(AUTHORITY, protoRoutes)))));
responseObserver.onNext(
buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0000"));
RouteMatch routeMatch2 =
new RouteMatch(
new PathMatcher(null, "/", null), Collections.<HeaderMatcher>emptyList(),
new FractionMatcher(10, 100));
Map<String, ?> rawRoute2 = XdsNameResolver.convertToRawRoute(routeMatch2, "action_foo");
Map<String, ?> rawMatchFraction = (Map<String, ?>) rawRoute2.get("matchFraction");
assertThat(rawMatchFraction).containsExactly("numerator", 10, "denominator", 100);
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();
RouteMatch routeMatch3 =
new RouteMatch(
new PathMatcher(null, "/", null),
Arrays.asList(
new HeaderMatcher("timeout", null, null, new HeaderMatcher.Range(0L, 10L),
null, null, null, false)),
null);
Map<String, ?> rawRoute3 = XdsNameResolver.convertToRawRoute(routeMatch3, "action_foo");
Map<String, ?> header =
(Map<String, ?>) Iterables.getOnlyElement((List<?>) rawRoute3.get("headers"));
assertThat((Map<String, ?>) header.get("rangeMatch")).containsExactly("start", 0L, "end", 10L);
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", "fooSvc", "method", "hello");
String action2 = (String) routes.get(2).get("action");
assertThat((Map<String, ?>) routes.get(3).get("methodName"))
.containsExactly("service", "fooSvc", "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);
RouteMatch routeMatch4 =
new RouteMatch(
new PathMatcher(null, "/", null),
Arrays.asList(
new HeaderMatcher(":scheme", "https", null, null, null, null, null, false),
new HeaderMatcher(
":path", null, Pattern.compile("google.*"), null, null, null, null, true),
new HeaderMatcher("timeout", null, null, null, true, null, null, false),
new HeaderMatcher(":authority", null, null, null, null, "google", null, false),
new HeaderMatcher(":authority", null, null, null, null, null, "grpc.io", false)),
null);
String expectedJson4 = "{\n"
+ " \"prefix\": \"/\",\n"
+ " \"headers\": [\n"
+ " {\n"
+ " \"name\": \":scheme\",\n"
+ " \"exactMatch\": \"https\",\n"
+ " \"invertMatch\": false\n"
+ " },\n"
+ " {\n"
+ " \"name\": \":path\",\n"
+ " \"regexMatch\": \"google.*\",\n"
+ " \"invertMatch\": true\n"
+ " },\n"
+ " {\n"
+ " \"name\": \"timeout\",\n"
+ " \"presentMatch\": true,\n"
+ " \"invertMatch\": false\n"
+ " },\n"
+ " {\n"
+ " \"name\": \":authority\",\n"
+ " \"prefixMatch\": \"google\",\n"
+ " \"invertMatch\": false\n"
+ " },\n"
+ " {\n"
+ " \"name\": \":authority\",\n"
+ " \"suffixMatch\": \"grpc.io\",\n"
+ " \"invertMatch\": false\n"
+ " }\n"
+ " ],\n"
+ " \"action\": \"action_foo\""
+ "}";
assertThat(XdsNameResolver.convertToRawRoute(routeMatch4, "action_foo"))
.isEqualTo(JsonParser.parse(expectedJson4));
}
/** 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) {
static void assertCdsPolicy(Map<String, ?> action, String clusterName) {
assertThat(action.keySet()).containsExactly("childPolicy");
Map<String, ?> lbConfig =
Iterables.getOnlyElement((List<Map<String, ?>>) action.get("childPolicy"));
@ -595,7 +245,7 @@ public class XdsNameResolverTest {
* to weight mapping.
*/
@SuppressWarnings("unchecked")
private static void assertWeightedTargetPolicy(
static void assertWeightedTargetPolicy(
Map<String, ?> action, Map<String, Integer> clusterWeights) {
assertThat(action.keySet()).containsExactly("childPolicy");
Map<String, ?> lbConfig =
@ -610,7 +260,7 @@ public class XdsNameResolverTest {
* mapping.
*/
@SuppressWarnings("unchecked")
private static void assertWeightedTargetConfigClusterWeights(
static void assertWeightedTargetConfigClusterWeights(
Map<String, ?> rawConfigValues, Map<String, Integer> clusterWeight) {
assertThat(rawConfigValues.keySet()).containsExactly("targets");
Map<String, ?> targets = (Map<String, ?>) rawConfigValues.get("targets");
@ -626,138 +276,4 @@ public class XdsNameResolverTest {
assertThat(target.get("weight")).isEqualTo(clusterWeight.get(targetName));
}
}
@Test
@SuppressWarnings("unchecked")
public void resolve_resourceNewlyAdded() {
xdsNameResolver.start(mockListener);
assertThat(responseObservers).hasSize(1);
StreamObserver<DiscoveryResponse> responseObserver = responseObservers.poll();
// Simulate receiving an LDS response that does not contain requested resource.
responseObserver.onNext(
buildLdsResponseForCluster("0", "bar.googleapis.com",
"cluster-bar.googleapis.com", "0000"));
fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS);
ArgumentCaptor<ResolutionResult> resolutionResultCaptor = ArgumentCaptor.forClass(null);
verify(mockListener).onResult(resolutionResultCaptor.capture());
ResolutionResult result = resolutionResultCaptor.getValue();
assertThat(result.getAddresses()).isEmpty();
// Simulate receiving another LDS response that contains cluster resolution directly in-line.
responseObserver.onNext(
buildLdsResponseForCluster("1", AUTHORITY, "cluster-foo.googleapis.com",
"0001"));
verify(mockListener, times(2)).onResult(resolutionResultCaptor.capture());
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("cds_experimental");
Map<String, ?> rawConfigValues = (Map<String, ?>) lbConfig.get("cds_experimental");
assertThat(rawConfigValues).containsExactly("cluster", "cluster-foo.googleapis.com");
}
/**
* Builds an LDS DiscoveryResponse containing the mapping of given host to
* the given cluster name directly in-line. Clients receiving this response is
* able to resolve cluster name for the given host immediately.
*/
private static DiscoveryResponse buildLdsResponseForCluster(
String versionInfo, String host, String clusterName, String nonce) {
List<Any> listeners = ImmutableList.of(
Any.pack(buildListener(host, // target Listener resource
Any.pack(
HttpConnectionManager.newBuilder()
.setRouteConfig(
buildRouteConfiguration("route-foo.googleapis.com", // doesn't matter
ImmutableList.of(
buildVirtualHost(
ImmutableList.of(host), // exact match
clusterName))))
.build()))));
return buildDiscoveryResponse(versionInfo, listeners, XdsClientImpl.ADS_TYPE_URL_LDS, nonce);
}
/**
* Builds an LDS DiscoveryResponse containing the mapping of given host to
* the given RDS resource name. Clients receiving this response is able to
* send an RDS request for resolving the cluster name for the given host.
*/
private static DiscoveryResponse buildLdsResponseForRdsResource(
String versionInfo, String host, String routeConfigName, String nonce) {
Rds rdsConfig =
Rds.newBuilder()
// Must set to use ADS.
.setConfigSource(
ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance()))
.setRouteConfigName(routeConfigName)
.build();
List<Any> listeners = ImmutableList.of(
Any.pack(
buildListener(
host, Any.pack(HttpConnectionManager.newBuilder().setRds(rdsConfig).build()))));
return buildDiscoveryResponse(versionInfo, listeners, XdsClientImpl.ADS_TYPE_URL_LDS, nonce);
}
/**
* Builds an RDS DiscoveryResponse containing route configuration with the given name and a
* virtual host that matches the given host to the given cluster name.
*/
private static DiscoveryResponse buildRdsResponseForCluster(
String versionInfo,
String routeConfigName,
String host,
String clusterName,
String nonce) {
List<Any> routeConfigs = ImmutableList.of(
Any.pack(
buildRouteConfiguration(
routeConfigName,
ImmutableList.of(
buildVirtualHost(ImmutableList.of(host), clusterName)))));
return buildDiscoveryResponse(versionInfo, routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, nonce);
}
private static RouteMatch buildPathPrefixMatch(String service) {
return RouteMatch.newBuilder().setPrefix("/" + service + "/").build();
}
private static RouteMatch buildPathExactMatch(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.of(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();
}
}

View File

@ -19,8 +19,7 @@ package io.grpc.xds;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.re2j.Pattern;
import io.grpc.LoadBalancer;
import io.grpc.LoadBalancer.Helper;
import io.grpc.LoadBalancerProvider;
@ -28,10 +27,15 @@ import io.grpc.LoadBalancerRegistry;
import io.grpc.NameResolver.ConfigOrError;
import io.grpc.internal.JsonParser;
import io.grpc.internal.ServiceConfigUtil.PolicySelection;
import io.grpc.xds.XdsRoutingLoadBalancerProvider.MethodName;
import io.grpc.xds.RouteMatch.FractionMatcher;
import io.grpc.xds.RouteMatch.HeaderMatcher;
import io.grpc.xds.RouteMatch.PathMatcher;
import io.grpc.xds.XdsRoutingLoadBalancerProvider.Route;
import io.grpc.xds.XdsRoutingLoadBalancerProvider.XdsRoutingConfig;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -42,7 +46,7 @@ import org.junit.runners.JUnit4;
public class XdsRoutingLoadBalancerProviderTest {
@Test
public void parseWeightedTargetConfig() throws Exception {
public void parseXdsRoutingLoadBalancingPolicyConfig() throws Exception {
LoadBalancerRegistry lbRegistry = new LoadBalancerRegistry();
XdsRoutingLoadBalancerProvider xdsRoutingLoadBalancerProvider =
new XdsRoutingLoadBalancerProvider(lbRegistry);
@ -103,48 +107,126 @@ public class XdsRoutingLoadBalancerProviderTest {
lbRegistry.register(lbProviderFoo);
lbRegistry.register(lbProviderBar);
String xdsRoutingConfigJson = ("{"
+ " 'route' : ["
+ " {"
+ " 'methodName' : {'service' : 'service_foo', 'method' : 'method_foo'},"
+ " 'action' : 'action_foo'"
+ " },"
+ " {"
+ " 'methodName' : {'service' : '', 'method' : ''},"
+ " 'action' : 'action_bar'"
+ " }"
+ " ],"
+ " 'action' : {"
+ " 'action_foo' : {"
+ " 'childPolicy' : ["
+ " {'unsupported_policy' : {}},"
+ " {'foo_policy' : {}}"
+ " ]"
+ " },"
+ " 'action_bar' : {"
+ " 'childPolicy' : ["
+ " {'unsupported_policy' : {}},"
+ " {'bar_policy' : {}}"
+ " ]"
+ " }"
+ " }"
+ "}").replace("'", "\"");
String xdsRoutingConfigJson = ("{\n"
+ " 'route' : [\n"
+ " {\n"
+ " 'path' : '/service_1/method_1',\n"
+ " 'action' : 'action_foo'\n"
+ " },\n"
+ " {\n"
+ " 'path' : '/service_1/method_2',\n"
+ " 'headers' : [\n"
+ " {\n"
+ " 'name' : ':scheme',\n"
+ " 'exactMatch' : 'https'\n"
+ " }\n"
+ " ],\n"
+ " 'action' : 'action_bar'\n"
+ " },\n"
+ " {\n"
+ " 'prefix' : '/service_2/',\n"
+ " 'headers' : [\n"
+ " {\n"
+ " 'name' : ':path',\n"
+ " 'regexMatch' : 'google.*'\n"
+ " }\n"
+ " ],\n"
+ " 'matchFraction' : {\n"
+ " 'numerator' : 10,\n"
+ " 'denominator' : 100\n"
+ " },\n"
+ " 'action' : 'action_bar'\n"
+ " },\n"
+ " {\n"
+ " 'regex' : '^/service_2/method_3$',\n"
+ " 'headers' : [\n"
+ " {\n"
+ " 'name' : ':method',\n"
+ " 'presentMatch' : true,\n"
+ " 'invertMatch' : true\n"
+ " },\n"
+ " {\n"
+ " 'name' : 'timeout',\n"
+ " 'rangeMatch' : {\n"
+ " 'start' : 0,\n"
+ " 'end' : 10\n"
+ " }\n"
+ " }\n"
+ " ],\n"
+ " 'matchFraction' : {\n"
+ " 'numerator' : 55,\n"
+ " 'denominator' : 1000\n"
+ " },\n"
+ " 'action' : 'action_foo'\n"
+ " }\n"
+ " ],\n"
+ " 'action' : {\n"
+ " 'action_foo' : {\n"
+ " 'childPolicy' : [\n"
+ " {'unsupported_policy' : {}},\n"
+ " {'foo_policy' : {}}\n"
+ " ]\n"
+ " },\n"
+ " 'action_bar' : {\n"
+ " 'childPolicy' : [\n"
+ " {'unsupported_policy' : {}},\n"
+ " {'bar_policy' : {}}\n"
+ " ]\n"
+ " }\n"
+ " }\n"
+ "}\n").replace("'", "\"");
@SuppressWarnings("unchecked")
Map<String, ?> rawLbConfigMap = (Map<String, ?>) JsonParser.parse(xdsRoutingConfigJson);
ConfigOrError configOrError =
xdsRoutingLoadBalancerProvider.parseLoadBalancingPolicyConfig(rawLbConfigMap);
assertThat(configOrError).isEqualTo(
ConfigOrError.fromConfig(
new XdsRoutingConfig(
ImmutableList.of(
new Route("action_foo", new MethodName("service_foo", "method_foo")),
new Route("action_bar", new MethodName("", ""))),
ImmutableMap.of(
"action_foo",
new PolicySelection(lbProviderFoo, new HashMap<String, Object>(), fooConfig),
"action_bar",
new PolicySelection(
lbProviderBar, new HashMap<String, Object>(), barConfig)))));
assertThat(configOrError.getConfig()).isNotNull();
XdsRoutingConfig config = (XdsRoutingConfig) configOrError.getConfig();
List<Route> configRoutes = config.routes;
assertThat(configRoutes).hasSize(4);
assertThat(configRoutes.get(0)).isEqualTo(
new Route(
new RouteMatch(
new PathMatcher("/service_1/method_1", null, null),
Collections.<HeaderMatcher>emptyList(), null),
"action_foo"));
assertThat(configRoutes.get(1)).isEqualTo(
new Route(
new RouteMatch(
new PathMatcher("/service_1/method_2", null, null),
Arrays.asList(
new HeaderMatcher(":scheme", "https", null, null, null, null,
null, false)),
null),
"action_bar"));
assertThat(configRoutes.get(2)).isEqualTo(
new Route(
new RouteMatch(
new PathMatcher(null, "/service_2/", null),
Arrays.asList(
new HeaderMatcher(":path", null, Pattern.compile("google.*"), null,
null, null, null, false)),
new FractionMatcher(10, 100)),
"action_bar"));
assertThat(configRoutes.get(3)).isEqualTo(
new Route(
new RouteMatch(
new PathMatcher(null, null, Pattern.compile("^/service_2/method_3$")),
Arrays.asList(
new HeaderMatcher(":method", null, null, null,
true, null, null, true),
new HeaderMatcher("timeout", null, null,
new HeaderMatcher.Range(0, 10), null, null, null, false)),
new FractionMatcher(55, 1000)),
"action_foo"));
Map<String, PolicySelection> configActions = config.actions;
assertThat(configActions).hasSize(2);
assertThat(configActions).containsExactly(
"action_foo",
new PolicySelection(lbProviderFoo, new HashMap<String, Object>(), fooConfig),
"action_bar",
new PolicySelection(
lbProviderBar, new HashMap<String, Object>(), barConfig));
}
}

View File

@ -16,324 +16,13 @@
package io.grpc.xds;
import static com.google.common.truth.Truth.assertThat;
import static io.grpc.ConnectivityState.READY;
import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
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 io.grpc.Attributes;
import io.grpc.Attributes.Key;
import io.grpc.CallOptions;
import io.grpc.ChannelLogger;
import io.grpc.ConnectivityState;
import io.grpc.EquivalentAddressGroup;
import io.grpc.LoadBalancer;
import io.grpc.LoadBalancer.Helper;
import io.grpc.LoadBalancer.PickSubchannelArgs;
import io.grpc.LoadBalancer.ResolvedAddresses;
import io.grpc.LoadBalancer.Subchannel;
import io.grpc.LoadBalancer.SubchannelPicker;
import io.grpc.LoadBalancerProvider;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.MethodDescriptor.MethodType;
import io.grpc.Status;
import io.grpc.internal.ServiceConfigUtil.PolicySelection;
import io.grpc.internal.TestUtils;
import io.grpc.internal.TestUtils.StandardLoadBalancerProvider;
import io.grpc.testing.TestMethodDescriptors;
import io.grpc.xds.XdsRoutingLoadBalancerProvider.MethodName;
import io.grpc.xds.XdsRoutingLoadBalancerProvider.Route;
import io.grpc.xds.XdsRoutingLoadBalancerProvider.XdsRoutingConfig;
import io.grpc.xds.XdsSubchannelPickers.ErrorPicker;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.Ignore;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/** Tests for {@link XdsRoutingLoadBalancer}. */
@RunWith(JUnit4.class)
@Ignore
public class XdsRoutingLoadBalancerTest {
private final List<LoadBalancer> fooBalancers = new ArrayList<>();
private final List<LoadBalancer> barBalancers = new ArrayList<>();
private final List<LoadBalancer> bazBalancers = new ArrayList<>();
private final List<Helper> fooHelpers = new ArrayList<>();
private final List<Helper> barHelpers = new ArrayList<>();
private final List<Helper> bazHelpers = new ArrayList<>();
private final LoadBalancerProvider fooLbProvider =
new StandardLoadBalancerProvider("foo_policy") {
@Override
public LoadBalancer newLoadBalancer(Helper helper) {
LoadBalancer lb = mock(LoadBalancer.class);
fooBalancers.add(lb);
fooHelpers.add(helper);
return lb;
}
};
private final LoadBalancerProvider barLbProvider =
new StandardLoadBalancerProvider("bar_policy") {
@Override
public LoadBalancer newLoadBalancer(Helper helper) {
LoadBalancer lb = mock(LoadBalancer.class);
barBalancers.add(lb);
barHelpers.add(helper);
return lb;
}
};
private final LoadBalancerProvider bazLbProvider =
new StandardLoadBalancerProvider("baz_policy") {
@Override
public LoadBalancer newLoadBalancer(Helper helper) {
LoadBalancer lb = mock(LoadBalancer.class);
bazBalancers.add(lb);
bazHelpers.add(helper);
return lb;
}
};
@Mock
private Helper helper;
@Mock
private ChannelLogger channelLogger;
private LoadBalancer xdsRoutingLb;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
doReturn(channelLogger).when(helper).getChannelLogger();
xdsRoutingLb = new XdsRoutingLoadBalancer(helper);
}
@After
public void tearDown() {
xdsRoutingLb.shutdown();
for (LoadBalancer balancer : Iterables.concat(fooBalancers, barBalancers, bazBalancers)) {
verify(balancer).shutdown();
}
}
@Test
public void typicalWorkflow() {
// Resolution error.
xdsRoutingLb.handleNameResolutionError(Status.UNAUTHENTICATED);
verify(helper).updateBalancingState(eq(TRANSIENT_FAILURE), any(SubchannelPicker.class));
// Config update.
Attributes attributes =
Attributes.newBuilder().set(Key.<String>create("fakeKey"), "fakeVal").build();
Object fooConfig1 = new Object();
Object barConfig1 = new Object();
Object bazConfig1 = new Object();
Object fooConfig2 = new Object();
XdsRoutingConfig xdsRoutingConfig = new XdsRoutingConfig(
ImmutableList.of(
new Route("foo_action", new MethodName("service1", "method1")),
new Route("foo_action", new MethodName("service2", "method2")),
new Route("bar_action", new MethodName("service1", "hello")),
new Route("bar_action", new MethodName("service2", "hello")),
new Route("foo_action_2", new MethodName("service2", "")),
new Route("baz_action", new MethodName("", ""))),
ImmutableMap.of(
"foo_action",
new PolicySelection(fooLbProvider, null, fooConfig1),
"foo_action_2",
new PolicySelection(fooLbProvider, null, fooConfig2),
"bar_action",
new PolicySelection(barLbProvider, null, barConfig1),
"baz_action",
new PolicySelection(bazLbProvider, null, bazConfig1)));
xdsRoutingLb.handleResolvedAddresses(
ResolvedAddresses.newBuilder()
.setAddresses(ImmutableList.<EquivalentAddressGroup>of())
.setAttributes(attributes)
.setLoadBalancingPolicyConfig(xdsRoutingConfig).build());
assertThat(fooBalancers).hasSize(2);
ArgumentCaptor<ResolvedAddresses> resolvedAddressesCaptor = ArgumentCaptor.forClass(null);
verify(fooBalancers.get(0)).handleResolvedAddresses(resolvedAddressesCaptor.capture());
ResolvedAddresses resolvedAddressesFoo0 = resolvedAddressesCaptor.getValue();
verify(fooBalancers.get(1)).handleResolvedAddresses(resolvedAddressesCaptor.capture());
ResolvedAddresses resolvedAddressesFoo1 = resolvedAddressesCaptor.getValue();
assertThat(barBalancers).hasSize(1);
verify(barBalancers.get(0)).handleResolvedAddresses(resolvedAddressesCaptor.capture());
ResolvedAddresses resolvedAddressesBar = resolvedAddressesCaptor.getValue();
assertThat(bazBalancers).hasSize(1);
verify(bazBalancers.get(0)).handleResolvedAddresses(resolvedAddressesCaptor.capture());
ResolvedAddresses resolvedAddressesBaz = resolvedAddressesCaptor.getValue();
assertThat(resolvedAddressesFoo0.getAttributes()).isEqualTo(attributes);
assertThat(resolvedAddressesFoo1.getAttributes()).isEqualTo(attributes);
assertThat(resolvedAddressesBar.getAttributes()).isEqualTo(attributes);
assertThat(resolvedAddressesBaz.getAttributes()).isEqualTo(attributes);
assertThat(
Arrays.asList(
resolvedAddressesFoo0.getLoadBalancingPolicyConfig(),
resolvedAddressesFoo1.getLoadBalancingPolicyConfig()))
.containsExactly(fooConfig1, fooConfig2);
LoadBalancer fooBalancer1;
Helper fooHelper1;
Helper fooHelper2;
if (resolvedAddressesFoo0.getLoadBalancingPolicyConfig().equals(fooConfig1)) {
fooBalancer1 = fooBalancers.get(0);
fooHelper1 = fooHelpers.get(0);
fooHelper2 = fooHelpers.get(1);
} else {
fooBalancer1 = fooBalancers.get(1);
fooHelper1 = fooHelpers.get(1);
fooHelper2 = fooHelpers.get(0);
}
assertThat(resolvedAddressesBar.getLoadBalancingPolicyConfig()).isEqualTo(barConfig1);
assertThat(resolvedAddressesBaz.getLoadBalancingPolicyConfig()).isEqualTo(bazConfig1);
Helper barHelper = barHelpers.get(0);
Helper bazHelper = bazHelpers.get(0);
// State update.
Subchannel subchannelFoo1 = mock(Subchannel.class);
Subchannel subchannelFoo2 = mock(Subchannel.class);
fooHelper1.updateBalancingState(READY, TestUtils.pickerOf(subchannelFoo1));
fooHelper2.updateBalancingState(READY, TestUtils.pickerOf(subchannelFoo2));
barHelper.updateBalancingState(
TRANSIENT_FAILURE, new ErrorPicker(Status.ABORTED.withDescription("abort bar")));
bazHelper.updateBalancingState(
TRANSIENT_FAILURE, new ErrorPicker(Status.DATA_LOSS.withDescription("data loss baz")));
ArgumentCaptor<ConnectivityState> connectivityStateCaptor = ArgumentCaptor.forClass(null);
ArgumentCaptor<SubchannelPicker> subchannelPickerCaptor = ArgumentCaptor.forClass(null);
verify(helper, atLeastOnce()).updateBalancingState(
connectivityStateCaptor.capture(), subchannelPickerCaptor.capture());
assertThat(connectivityStateCaptor.getValue()).isEqualTo(READY);
SubchannelPicker picker = subchannelPickerCaptor.getValue();
assertPickerRoutePathToSubchannel(picker, "service1", "method1", subchannelFoo1);
assertPickerRoutePathToSubchannel(picker, "service2", "method2", subchannelFoo1);
assertPickerRoutePathToError(
picker, "service1", "hello", Status.ABORTED.withDescription("abort bar"));
assertPickerRoutePathToError(
picker, "service2", "hello", Status.ABORTED.withDescription("abort bar"));
assertPickerRoutePathToSubchannel(picker, "service2", "otherMethod", subchannelFoo2);
assertPickerRoutePathToError(
picker, "otherService", "hello", Status.DATA_LOSS.withDescription("data loss baz"));
// Resolution error.
Status error = Status.UNAVAILABLE.withDescription("fake unavailable");
xdsRoutingLb.handleNameResolutionError(error);
for (LoadBalancer lb : Iterables.concat(fooBalancers, barBalancers, bazBalancers)) {
verify(lb).handleNameResolutionError(error);
}
// New config update.
Object fooConfig3 = new Object();
Object barConfig2 = new Object();
Object barConfig3 = new Object();
Object bazConfig2 = new Object();
xdsRoutingConfig = new XdsRoutingConfig(
ImmutableList.of(
new Route("foo_action", new MethodName("service1", "method1")),
new Route("foo_action", new MethodName("service2", "method3")),
new Route("bar_action", new MethodName("service1", "hello")),
new Route("bar_action_2", new MethodName("service2", "hello")),
new Route("baz_action", new MethodName("", ""))),
ImmutableMap.of(
"foo_action",
new PolicySelection(fooLbProvider, null, fooConfig3),
"bar_action",
new PolicySelection(barLbProvider, null, barConfig2),
"bar_action_2",
new PolicySelection(barLbProvider, null, barConfig3),
"baz_action",
new PolicySelection(bazLbProvider, null, bazConfig2)));
xdsRoutingLb.handleResolvedAddresses(
ResolvedAddresses.newBuilder()
.setAddresses(ImmutableList.<EquivalentAddressGroup>of())
.setLoadBalancingPolicyConfig(xdsRoutingConfig)
.build());
verify(fooBalancer1, times(2)).handleResolvedAddresses(resolvedAddressesCaptor.capture());
assertThat(resolvedAddressesCaptor.getValue().getLoadBalancingPolicyConfig())
.isEqualTo(fooConfig3);
assertThat(barBalancers).hasSize(2);
verify(barBalancers.get(0), times(2))
.handleResolvedAddresses(resolvedAddressesCaptor.capture());
assertThat(resolvedAddressesCaptor.getValue().getLoadBalancingPolicyConfig())
.isEqualTo(barConfig2);
verify(barBalancers.get(1)).handleResolvedAddresses(resolvedAddressesCaptor.capture());
assertThat(resolvedAddressesCaptor.getValue().getLoadBalancingPolicyConfig())
.isEqualTo(barConfig3);
verify(bazBalancers.get(0), times(2))
.handleResolvedAddresses(resolvedAddressesCaptor.capture());
assertThat(resolvedAddressesCaptor.getValue().getLoadBalancingPolicyConfig())
.isEqualTo(bazConfig2);
// New status update.
Subchannel subchannelBar2 = mock(Subchannel.class);
Helper barHelper2 = barHelpers.get(1);
barHelper2.updateBalancingState(READY, TestUtils.pickerOf(subchannelBar2));
verify(helper, atLeastOnce()).updateBalancingState(
connectivityStateCaptor.capture(), subchannelPickerCaptor.capture());
assertThat(connectivityStateCaptor.getValue()).isEqualTo(READY);
picker = subchannelPickerCaptor.getValue();
assertPickerRoutePathToSubchannel(picker, "service1", "method1", subchannelFoo1);
assertPickerRoutePathToError(
picker, "service1", "method2", Status.DATA_LOSS.withDescription("data loss baz"));
assertPickerRoutePathToSubchannel(picker, "service2", "method3", subchannelFoo1);
assertPickerRoutePathToError(
picker, "service1", "hello", Status.ABORTED.withDescription("abort bar"));
assertPickerRoutePathToSubchannel(picker, "service2", "hello", subchannelBar2);
}
private static PickSubchannelArgs pickSubchannelArgsForMethod(
final String service, final String method) {
return new PickSubchannelArgs() {
@Override
public CallOptions getCallOptions() {
return CallOptions.DEFAULT;
}
@Override
public Metadata getHeaders() {
return new Metadata();
}
@Override
public MethodDescriptor<Void, Void> getMethodDescriptor() {
return MethodDescriptor.<Void, Void>newBuilder()
.setType(MethodType.UNARY)
.setFullMethodName(service + "/" + method)
.setRequestMarshaller(TestMethodDescriptors.voidMarshaller())
.setResponseMarshaller(TestMethodDescriptors.voidMarshaller())
.build();
}
};
}
private static void assertPickerRoutePathToSubchannel(
SubchannelPicker picker, String service, String method, Subchannel expectedSubchannel) {
Subchannel actualSubchannel =
picker.pickSubchannel(pickSubchannelArgsForMethod(service, method)).getSubchannel();
assertThat(actualSubchannel).isEqualTo(expectedSubchannel);
}
private static void assertPickerRoutePathToError(
SubchannelPicker picker, String service, String method, Status expectedStatus) {
Status actualStatus =
picker.pickSubchannel(pickSubchannelArgsForMethod(service, method)).getStatus();
assertThat(actualStatus.getCode()).isEqualTo(expectedStatus.getCode());
assertThat(actualStatus.getDescription()).isEqualTo(expectedStatus.getDescription());
}
// TODO(chengyuanzhang)
}