mirror of https://github.com/grpc/grpc-java.git
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:
parent
417d7700dd
commit
c551fe3807
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue