From c551fe38072d5c4989f58099b9047f7cf409fd26 Mon Sep 17 00:00:00 2001 From: Chengyuan Zhang Date: Thu, 4 Jun 2020 09:03:49 +0000 Subject: [PATCH] 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. --- .../main/java/io/grpc/xds/EnvoyProtoData.java | 395 ++------ xds/src/main/java/io/grpc/xds/RouteMatch.java | 372 ++++++++ .../java/io/grpc/xds/XdsNameResolver.java | 144 ++- .../io/grpc/xds/XdsRoutingLoadBalancer.java | 41 +- .../xds/XdsRoutingLoadBalancerProvider.java | 296 +++--- .../java/io/grpc/xds/EnvoyProtoDataTest.java | 65 +- .../java/io/grpc/xds/XdsClientImplTest.java | 8 +- .../xds/XdsNameResolverIntegrationTest.java | 626 +++++++++++++ .../java/io/grpc/xds/XdsNameResolverTest.java | 844 ++++-------------- .../XdsRoutingLoadBalancerProviderTest.java | 166 +++- .../grpc/xds/XdsRoutingLoadBalancerTest.java | 317 +------ 11 files changed, 1705 insertions(+), 1569 deletions(-) create mode 100644 xds/src/main/java/io/grpc/xds/RouteMatch.java create mode 100644 xds/src/test/java/io/grpc/xds/XdsNameResolverIntegrationTest.java diff --git a/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java b/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java index 94b22e887a..7b48117f4c 100644 --- a/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java +++ b/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java @@ -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 fromEnvoyProtoRoute(io.envoyproxy.envoy.api.v2.route.Route proto) { - StructOrError routeMatch = RouteMatch.fromEnvoyProtoRouteMatch(proto.getMatch()); + StructOrError 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 headerMatchers; - @Nullable - private final Fraction fractionMatch; - - @VisibleForTesting - RouteMatch( - @Nullable String pathPrefixMatch, @Nullable String pathExactMatch, - @Nullable Pattern pathSafeRegExMatch, @Nullable Fraction fractionMatch, - List 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.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 fromEnvoyProtoRouteMatch( + static StructOrError 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 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 parsedFraction = + convertEnvoyProtoFraction(proto.getRuntimeFraction().getDefaultValue()); + if (parsedFraction.getErrorDetail() != null) { + return StructOrError.fromError(parsedFraction.getErrorDetail()); + } + fractionMatch = parsedFraction.getStruct(); } List headerMatchers = new ArrayList<>(); for (io.envoyproxy.envoy.api.v2.route.HeaderMatcher hmProto : proto.getHeadersList()) { - StructOrError headerMatcher = - HeaderMatcher.fromEnvoyProtoHeaderMatcher(hmProto); + StructOrError 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 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 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 fromEnvoyProtoHeaderMatcher( + static StructOrError 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: diff --git a/xds/src/main/java/io/grpc/xds/RouteMatch.java b/xds/src/main/java/io/grpc/xds/RouteMatch.java new file mode 100644 index 0000000000..63af1629f8 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/RouteMatch.java @@ -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 headerMatchers; + @Nullable + private final FractionMatcher fractionMatch; + + @VisibleForTesting + RouteMatch(PathMatcher pathMatch, List 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.emptyList(), null); + } + + PathMatcher getPathMatch() { + return pathMatch; + } + + List 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(); + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 42e0fbadcd..d37ab35e7c 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -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 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 generateXdsRoutingRawConfig(List routesUpdate) { - List routes = new ArrayList<>(routesUpdate.size()); - Map actions = new LinkedHashMap<>(); - Map 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 methodName = ImmutableMap.of("service", service, "method", method); - String actionName; + @VisibleForTesting + static ImmutableMap generateXdsRoutingRawConfig(List routes) { + List rawRoutes = new ArrayList<>(); + Map rawActions = new LinkedHashMap<>(); + Map existingActions = new HashMap<>(); + for (Route route : routes) { RouteAction routeAction = route.getRouteAction(); + String actionName; Map 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 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 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 generateWeightedTargetRawConfig( + @VisibleForTesting + static ImmutableMap convertToRawRoute(RouteMatch routeMatch, String actionName) { + ImmutableMap.Builder 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 rawHeaderMatcherListBuilder = new ImmutableList.Builder<>(); + List headerMatchers = routeMatch.getHeaderMatchers(); + for (HeaderMatcher headerMatcher : headerMatchers) { + ImmutableMap.Builder 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 generateWeightedTargetRawConfig( List clusterWeights) { Map targets = new LinkedHashMap<>(); for (ClusterWeight clusterWeight : clusterWeights) { diff --git a/xds/src/main/java/io/grpc/xds/XdsRoutingLoadBalancer.java b/xds/src/main/java/io/grpc/xds/XdsRoutingLoadBalancer.java index 10050618e2..2038179c4a 100644 --- a/xds/src/main/java/io/grpc/xds/XdsRoutingLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/XdsRoutingLoadBalancer.java @@ -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 routePickers = new LinkedHashMap<>(); + Map 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 routePickers; + final Map 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 routePickers) { + RouteMatchingSubchannelPicker(Map 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")); } } } diff --git a/xds/src/main/java/io/grpc/xds/XdsRoutingLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/XdsRoutingLoadBalancerProvider.java index 2f62807699..17e8d9b6c8 100644 --- a/xds/src/main/java/io/grpc/xds/XdsRoutingLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/XdsRoutingLoadBalancerProvider.java @@ -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 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> routes = JsonUtil.getListOfObjects(rawConfig, "route"); - if (routes == null || routes.isEmpty()) { + List parsedRoutes = new ArrayList<>(); + List> 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 parsedRoutes = new ArrayList<>(); - Set methodNames = new HashSet<>(); - for (int i = 0; i < routes.size(); i++) { - Map 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 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 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 rawAction, LoadBalancerRegistry registry) { + List 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 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 headers = new ArrayList<>(); + List> rawHeaders = JsonUtil.getListOfObjects(rawRoute, "headers"); + if (rawHeaders != null) { + for (Map rawHeader : rawHeaders) { + HeaderMatcher headerMatcher = parseHeaderMatcher(rawHeader); + headers.add(headerMatcher); + } + } + + FractionMatcher matchFraction = null; + Map 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 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 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 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 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 routes; final Map 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 routes, Map actions) { + private XdsRoutingConfig(List routes, Map 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(); } } diff --git a/xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java b/xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java index ee94f58939..924786d3ec 100644 --- a/xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java +++ b/xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java @@ -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.emptyList()), + new RouteMatch(new PathMatcher("/service/method", null, null), + Collections.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 struct1 = RouteMatch.fromEnvoyProtoRouteMatch(proto1); + StructOrError struct1 = Route.convertEnvoyProtoRouteMatch(proto1); assertThat(struct1.getErrorDetail()).isNull(); assertThat(struct1.getStruct()).isEqualTo( - new RouteMatch("/", null, null, null, Collections.emptyList())); + new RouteMatch( + new PathMatcher(null, "/", null), Collections.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 struct2 = RouteMatch.fromEnvoyProtoRouteMatch(proto2); + StructOrError struct2 = Route.convertEnvoyProtoRouteMatch(proto2); assertThat(struct2.getErrorDetail()).isNull(); assertThat(struct2.getStruct()).isEqualTo( new RouteMatch( - null, "/service/method", null, null, Collections.emptyList())); + new PathMatcher("/service/method", null, null), + Collections.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 struct3 = RouteMatch.fromEnvoyProtoRouteMatch(proto3); + StructOrError 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 struct4 = RouteMatch.fromEnvoyProtoRouteMatch(proto4); + StructOrError struct4 = Route.convertEnvoyProtoRouteMatch(proto4); assertThat(struct4.getErrorDetail()).isNull(); assertThat(struct4.getStruct()).isEqualTo( new RouteMatch( - null, null, Pattern.compile("."), null, Collections.emptyList())); + new PathMatcher(null, null, Pattern.compile(".")), + Collections.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 struct5 = RouteMatch.fromEnvoyProtoRouteMatch(proto5); + StructOrError 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 struct6 = RouteMatch.fromEnvoyProtoRouteMatch(proto6); + StructOrError 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 unsetStruct = RouteMatch.fromEnvoyProtoRouteMatch(unsetProto); + StructOrError 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 struct = RouteMatch.fromEnvoyProtoRouteMatch(proto); + StructOrError 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 struct = RouteMatch.fromEnvoyProtoRouteMatch(proto); + StructOrError struct = Route.convertEnvoyProtoRouteMatch(proto); assertThat(struct.getErrorDetail()).isNull(); assertThat(struct.getStruct()) .isEqualTo( new RouteMatch( - "", null, null, new RouteMatch.Fraction(30, 100), - Collections.emptyList())); + new PathMatcher(null, "", null), Collections.emptyList(), + new FractionMatcher(30, 100))); } @Test @@ -345,7 +351,7 @@ public class EnvoyProtoDataTest { .setName(":method") .setExactMatch("PUT") .build(); - StructOrError struct1 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto1); + StructOrError 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 struct2 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto2); + StructOrError 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 struct3 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto3); + StructOrError 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 struct4 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto4); + StructOrError 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 struct5 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto5); + StructOrError 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 struct6 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto6); + StructOrError 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 struct7 = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto7); + StructOrError 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 unsetStruct = - HeaderMatcher.fromEnvoyProtoHeaderMatcher(unsetProto); + StructOrError 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 struct = HeaderMatcher.fromEnvoyProtoHeaderMatcher(proto); + StructOrError struct = Route.convertEnvoyProtoHeaderMatcher(proto); assertThat(struct.getErrorDetail()).isNotNull(); assertThat(struct.getStruct()).isNull(); } diff --git a/xds/src/test/java/io/grpc/xds/XdsClientImplTest.java b/xds/src/test/java/io/grpc/xds/XdsClientImplTest.java index f67232f624..b9fb294969 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientImplTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientImplTest.java @@ -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( diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverIntegrationTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverIntegrationTest.java new file mode 100644 index 0000000000..619fd3c4e0 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverIntegrationTest.java @@ -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> responseObservers = new ArrayDeque<>(); + private final ServiceConfigParser serviceConfigParser = new ServiceConfigParser() { + @Override + public ConfigOrError parseServiceConfig(Map 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 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 streamAggregatedResources( + final StreamObserver responseObserver) { + responseObservers.offer(responseObserver); + @SuppressWarnings("unchecked") + StreamObserver 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 servers) { + assertThat(Iterables.getOnlyElement(servers).getServerUri()).isEqualTo(serverName); + return channel; + } + }; + Bootstrapper bootstrapper = new Bootstrapper() { + @Override + public BootstrapInfo readBootstrap() { + List serverList = + ImmutableList.of( + new ServerInfo(serverName, + ImmutableList.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.of(), FAKE_BOOTSTRAP_NODE); + } + }; + + XdsNameResolver resolver = + new XdsNameResolver( + AUTHORITY, + args, + backoffPolicyProvider, + fakeClock.getStopwatchSupplier(), + channelFactory, + bootstrapper); + resolver.start(mockListener); + ArgumentCaptor 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 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 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 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 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) result.getServiceConfig().getConfig()).isEmpty(); + } + + @Test + @SuppressWarnings("unchecked") + public void resolve_resourceUpdated() { + xdsNameResolver.start(mockListener); + assertThat(responseObservers).hasSize(1); + StreamObserver 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 serviceConfig = (Map) result.getServiceConfig().getConfig(); + + List> rawLbConfigs = + (List>) serviceConfig.get("loadBalancingConfig"); + Map lbConfig = Iterables.getOnlyElement(rawLbConfigs); + assertThat(lbConfig.keySet()).containsExactly("cds_experimental"); + Map rawConfigValues = (Map) 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) result.getServiceConfig().getConfig(); + rawLbConfigs = (List>) serviceConfig.get("loadBalancingConfig"); + lbConfig = Iterables.getOnlyElement(rawLbConfigs); + assertThat(lbConfig.keySet()).containsExactly("cds_experimental"); + rawConfigValues = (Map) 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 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 serviceConfig = (Map) result.getServiceConfig().getConfig(); + List> rawLbConfigs = + (List>) serviceConfig.get("loadBalancingConfig"); + Map lbConfig = Iterables.getOnlyElement(rawLbConfigs); + assertThat(lbConfig.keySet()).containsExactly("cds_experimental"); + Map rawConfigValues = (Map) 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 responseObserver = responseObservers.poll(); + + // Simulate receiving an LDS response that contains routes resolution directly in-line. + List 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 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 serviceConfig = (Map) result.getServiceConfig().getConfig(); + + List> rawLbConfigs = + (List>) serviceConfig.get("loadBalancingConfig"); + Map lbConfig = Iterables.getOnlyElement(rawLbConfigs); + assertThat(lbConfig.keySet()).containsExactly("xds_routing_experimental"); + Map rawConfigValues = (Map) lbConfig.get("xds_routing_experimental"); + assertThat(rawConfigValues.keySet()).containsExactly("action", "route"); + Map> actions = + (Map>) rawConfigValues.get("action"); + List> routes = (List>) rawConfigValues.get("route"); + assertThat(actions).hasSize(4); + assertThat(routes).hasSize(5); + + Map 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 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 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 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 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 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 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 serviceConfig = (Map) result.getServiceConfig().getConfig(); + List> rawLbConfigs = + (List>) serviceConfig.get("loadBalancingConfig"); + Map lbConfig = Iterables.getOnlyElement(rawLbConfigs); + assertThat(lbConfig.keySet()).containsExactly("weighted_target_experimental"); + Map rawConfigValues = (Map) 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 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 serviceConfig = (Map) result.getServiceConfig().getConfig(); + List> rawLbConfigs = + (List>) serviceConfig.get("loadBalancingConfig"); + Map lbConfig = Iterables.getOnlyElement(rawLbConfigs); + assertThat(lbConfig.keySet()).containsExactly("cds_experimental"); + Map rawConfigValues = (Map) 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 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 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 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 clusterWeights) { + WeightedCluster.Builder builder = WeightedCluster.newBuilder(); + for (Map.Entry 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 routes) { + return VirtualHost.newBuilder() + .setName("virtualhost00.googleapis.com") // don't care + .addAllDomains(ImmutableList.of(domain)) + .addAllRoutes(routes) + .build(); + } +} diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index aeb373c2a8..584a53a8f6 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -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> responseObservers = new ArrayDeque<>(); - private final ServiceConfigParser serviceConfigParser = new ServiceConfigParser() { - @Override - public ConfigOrError parseServiceConfig(Map 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 streamAggregatedResources( - final StreamObserver responseObserver) { - responseObservers.offer(responseObserver); - @SuppressWarnings("unchecked") - StreamObserver 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 servers) { - assertThat(Iterables.getOnlyElement(servers).getServerUri()).isEqualTo(serverName); - return channel; - } - }; - Bootstrapper bootstrapper = new Bootstrapper() { - @Override - public BootstrapInfo readBootstrap() { - List serverList = - ImmutableList.of( - new ServerInfo(serverName, - ImmutableList.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.of(), FAKE_BOOTSTRAP_NODE); - } - }; - - XdsNameResolver resolver = - new XdsNameResolver( - AUTHORITY, - args, - backoffPolicyProvider, - fakeClock.getStopwatchSupplier(), - channelFactory, - bootstrapper); - resolver.start(mockListener); - ArgumentCaptor 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 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 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 resolutionResultCaptor = ArgumentCaptor.forClass(null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); - ResolutionResult result = resolutionResultCaptor.getValue(); - ObjectPool xdsClientPool = result.getAttributes().get(XdsAttributes.XDS_CLIENT_POOL); - assertThat(xdsClientPool).isNotNull(); - } - - @Test - public void resolve_foundResource() { - xdsNameResolver.start(mockListener); - assertThat(responseObservers).hasSize(1); - StreamObserver 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 resolutionResultCaptor = ArgumentCaptor.forClass(null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); - ResolutionResult result = resolutionResultCaptor.getValue(); - assertThat(result.getAddresses()).isEmpty(); + public void generateWeightedTargetRawConfig() throws IOException { + List clusterWeights = + Arrays.asList( + new ClusterWeight("cluster-foo", 30), new ClusterWeight("cluster-bar", 50)); + Map 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 responseObserver = responseObservers.poll(); + public void generateXdsRoutingRawConfig() { + Route r1 = + new Route( + new RouteMatch( + new PathMatcher(null, "", null), Collections.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 config = + XdsNameResolver.generateXdsRoutingRawConfig(Arrays.asList(r1, r2)); + assertThat(config.keySet()).containsExactly("xds_routing_experimental"); + Map content = (Map) config.get(XDS_ROUTING_POLICY_NAME); + assertThat(content.keySet()).containsExactly("action", "route"); + Map> actions = (Map>) content.get("action"); + List> routes = (List>) content.get("route"); + assertThat(actions).hasSize(2); + assertThat(routes).hasSize(2); - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - ArgumentCaptor resolutionResultCaptor = ArgumentCaptor.forClass(null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); - ResolutionResult result = resolutionResultCaptor.getValue(); - assertThat(result.getAddresses()).isEmpty(); - assertThat((Map) result.getServiceConfig().getConfig()).isEmpty(); + Map route0 = routes.get(0); + assertThat(route0.keySet()).containsExactly("prefix", "matchFraction", "action"); + assertThat((String) route0.get("prefix")).isEqualTo(""); + assertThat((Map) route0.get("matchFraction")) + .containsExactly("numerator", 10, "denominator", 20); + assertCdsPolicy(actions.get(route0.get("action")), "cluster-foo"); + + Map route1 = routes.get(1); + assertThat(route1.keySet()).containsExactly("path", "headers", "action"); + assertThat((String) route1.get("path")).isEqualTo("/service/method"); + Map header = Iterables.getOnlyElement((List>) 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 responseObserver = responseObservers.poll(); + @Test + public void generateXdsRoutingRawConfig_allowDuplicateMatchers() { + Route route = + new Route( + new RouteMatch( + new PathMatcher("/service/method", null, null), + Collections.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 resolutionResultCaptor = ArgumentCaptor.forClass(null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); - ResolutionResult result = resolutionResultCaptor.getValue(); - assertThat(result.getAddresses()).isEmpty(); - Map serviceConfig = (Map) result.getServiceConfig().getConfig(); - - List> rawLbConfigs = - (List>) serviceConfig.get("loadBalancingConfig"); - Map lbConfig = Iterables.getOnlyElement(rawLbConfigs); - assertThat(lbConfig.keySet()).containsExactly("cds_experimental"); - Map rawConfigValues = (Map) 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) result.getServiceConfig().getConfig(); - rawLbConfigs = (List>) serviceConfig.get("loadBalancingConfig"); - lbConfig = Iterables.getOnlyElement(rawLbConfigs); - assertThat(lbConfig.keySet()).containsExactly("cds_experimental"); - rawConfigValues = (Map) lbConfig.get("cds_experimental"); - assertThat(rawConfigValues).containsExactly("cluster", "cluster-blade.googleapis.com"); + Map config = + XdsNameResolver.generateXdsRoutingRawConfig(Arrays.asList(route, route)); + assertThat(config.keySet()).containsExactly(XDS_ROUTING_POLICY_NAME); + Map content = (Map) config.get(XDS_ROUTING_POLICY_NAME); + assertThat(content.keySet()).containsExactly("action", "route"); + Map actions = (Map) 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 responseObserver = responseObservers.poll(); - - // Simulate receiving an LDS response that contains routes resolution directly in-line. - List 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 listeners = - ImmutableList.of(Any.pack(buildListener(AUTHORITY, Any.pack(httpConnectionManager)))); - responseObserver.onNext( - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000")); - - ArgumentCaptor resolutionResultCaptor = ArgumentCaptor.forClass(null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); - ResolutionResult result = resolutionResultCaptor.getValue(); - assertThat(result.getAddresses()).isEmpty(); - Map serviceConfig = (Map) result.getServiceConfig().getConfig(); - - List> rawLbConfigs = - (List>) serviceConfig.get("loadBalancingConfig"); - Map lbConfig = Iterables.getOnlyElement(rawLbConfigs); - assertThat(lbConfig.keySet()).containsExactly("xds_routing_experimental"); - Map rawConfigValues = (Map) lbConfig.get("xds_routing_experimental"); - assertThat(rawConfigValues.keySet()).containsExactly("action", "route"); - Map> actions = - (Map>) rawConfigValues.get("action"); - List> routes = (List>) rawConfigValues.get("route"); - assertThat(routes).hasSize(5); - for (Map route : routes) { - assertThat(route.keySet()).containsExactly("methodName", "action"); - } - assertThat((Map) routes.get(0).get("methodName")) - .containsExactly("service", "fooSvc", "method", "hello"); - String action0 = (String) routes.get(0).get("action"); - assertThat((Map) routes.get(1).get("methodName")) - .containsExactly("service", "fooSvc", "method", ""); - String action1 = (String) routes.get(1).get("action"); - assertThat((Map) routes.get(2).get("methodName")) - .containsExactly("service", "barSvc", "method", "hello"); - String action2 = (String) routes.get(2).get("action"); - assertThat((Map) routes.get(3).get("methodName")) - .containsExactly("service", "barSvc", "method", ""); - String action3 = (String) routes.get(3).get("action"); - assertThat((Map) 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 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) result.getServiceConfig().getConfig(); - rawLbConfigs = (List>) serviceConfig.get("loadBalancingConfig"); - lbConfig = Iterables.getOnlyElement(rawLbConfigs); - assertThat(lbConfig.keySet()).containsExactly(WEIGHTED_TARGET_POLICY_NAME); - rawConfigValues = (Map) 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 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.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 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 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.emptyList(), + new FractionMatcher(10, 100)); + Map rawRoute2 = XdsNameResolver.convertToRawRoute(routeMatch2, "action_foo"); + Map rawMatchFraction = (Map) rawRoute2.get("matchFraction"); + assertThat(rawMatchFraction).containsExactly("numerator", 10, "denominator", 100); - ArgumentCaptor resolutionResultCaptor = ArgumentCaptor.forClass(null); - verify(mockListener).onResult(resolutionResultCaptor.capture()); - ResolutionResult result = resolutionResultCaptor.getValue(); - assertThat(result.getAddresses()).isEmpty(); - Map serviceConfig = (Map) 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 rawRoute3 = XdsNameResolver.convertToRawRoute(routeMatch3, "action_foo"); + Map header = + (Map) Iterables.getOnlyElement((List) rawRoute3.get("headers")); + assertThat((Map) header.get("rangeMatch")).containsExactly("start", 0L, "end", 10L); - List> rawLbConfigs = - (List>) serviceConfig.get("loadBalancingConfig"); - Map lbConfig = Iterables.getOnlyElement(rawLbConfigs); - assertThat(lbConfig.keySet()).containsExactly("xds_routing_experimental"); - Map rawConfigValues = (Map) lbConfig.get("xds_routing_experimental"); - assertThat(rawConfigValues.keySet()).containsExactly("action", "route"); - Map> actions = - (Map>) rawConfigValues.get("action"); - List> routes = (List>) rawConfigValues.get("route"); - assertThat(routes).hasSize(5); - for (Map route : routes) { - assertThat(route.keySet()).containsExactly("methodName", "action"); - } - assertThat((Map) routes.get(0).get("methodName")) - .containsExactly("service", "fooSvc", "method", "hello"); - String action0 = (String) routes.get(0).get("action"); - assertThat((Map) routes.get(1).get("methodName")) - .containsExactly("service", "fooSvc", "method", ""); - String action1 = (String) routes.get(1).get("action"); - assertThat((Map) routes.get(2).get("methodName")) - .containsExactly("service", "fooSvc", "method", "hello"); - String action2 = (String) routes.get(2).get("action"); - assertThat((Map) routes.get(3).get("methodName")) - .containsExactly("service", "fooSvc", "method", ""); - String action3 = (String) routes.get(3).get("action"); - assertThat((Map) 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 action, String clusterName) { + static void assertCdsPolicy(Map action, String clusterName) { assertThat(action.keySet()).containsExactly("childPolicy"); Map lbConfig = Iterables.getOnlyElement((List>) action.get("childPolicy")); @@ -595,7 +245,7 @@ public class XdsNameResolverTest { * to weight mapping. */ @SuppressWarnings("unchecked") - private static void assertWeightedTargetPolicy( + static void assertWeightedTargetPolicy( Map action, Map clusterWeights) { assertThat(action.keySet()).containsExactly("childPolicy"); Map lbConfig = @@ -610,7 +260,7 @@ public class XdsNameResolverTest { * mapping. */ @SuppressWarnings("unchecked") - private static void assertWeightedTargetConfigClusterWeights( + static void assertWeightedTargetConfigClusterWeights( Map rawConfigValues, Map clusterWeight) { assertThat(rawConfigValues.keySet()).containsExactly("targets"); Map targets = (Map) 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 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 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 serviceConfig = (Map) result.getServiceConfig().getConfig(); - List> rawLbConfigs = - (List>) serviceConfig.get("loadBalancingConfig"); - Map lbConfig = Iterables.getOnlyElement(rawLbConfigs); - assertThat(lbConfig.keySet()).containsExactly("cds_experimental"); - Map rawConfigValues = (Map) 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 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 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 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 clusterWeights) { - WeightedCluster.Builder builder = WeightedCluster.newBuilder(); - for (Map.Entry 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 routes) { - return VirtualHost.newBuilder() - .setName("virtualhost00.googleapis.com") // don't care - .addAllDomains(ImmutableList.of(domain)) - .addAllRoutes(routes) - .build(); - } } diff --git a/xds/src/test/java/io/grpc/xds/XdsRoutingLoadBalancerProviderTest.java b/xds/src/test/java/io/grpc/xds/XdsRoutingLoadBalancerProviderTest.java index face3e1636..d600bafd69 100644 --- a/xds/src/test/java/io/grpc/xds/XdsRoutingLoadBalancerProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsRoutingLoadBalancerProviderTest.java @@ -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 rawLbConfigMap = (Map) 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(), fooConfig), - "action_bar", - new PolicySelection( - lbProviderBar, new HashMap(), barConfig))))); + assertThat(configOrError.getConfig()).isNotNull(); + XdsRoutingConfig config = (XdsRoutingConfig) configOrError.getConfig(); + List 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.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 configActions = config.actions; + assertThat(configActions).hasSize(2); + assertThat(configActions).containsExactly( + "action_foo", + new PolicySelection(lbProviderFoo, new HashMap(), fooConfig), + "action_bar", + new PolicySelection( + lbProviderBar, new HashMap(), barConfig)); } } diff --git a/xds/src/test/java/io/grpc/xds/XdsRoutingLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/XdsRoutingLoadBalancerTest.java index a13f5a23bd..df67155b02 100644 --- a/xds/src/test/java/io/grpc/xds/XdsRoutingLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsRoutingLoadBalancerTest.java @@ -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 fooBalancers = new ArrayList<>(); - private final List barBalancers = new ArrayList<>(); - private final List bazBalancers = new ArrayList<>(); - private final List fooHelpers = new ArrayList<>(); - private final List barHelpers = new ArrayList<>(); - private final List 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.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.of()) - .setAttributes(attributes) - .setLoadBalancingPolicyConfig(xdsRoutingConfig).build()); - assertThat(fooBalancers).hasSize(2); - ArgumentCaptor 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 connectivityStateCaptor = ArgumentCaptor.forClass(null); - ArgumentCaptor 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.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 getMethodDescriptor() { - return MethodDescriptor.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) }