diff --git a/build.gradle b/build.gradle index bab2ae0400..37cccdac8b 100644 --- a/build.gradle +++ b/build.gradle @@ -60,6 +60,7 @@ subprojects { protobufVersion = '3.12.0' protocVersion = protobufVersion opencensusVersion = '0.28.0' + autovalueVersion = '1.7.4' configureProtoCompilation = { String generatedSourcePath = "${projectDir}/src/generated" @@ -142,6 +143,8 @@ subprojects { libraries = [ android_annotations: "com.google.android:annotations:4.1.1.4", animalsniffer_annotations: "org.codehaus.mojo:animal-sniffer-annotations:1.19", + autovalue: "com.google.auto.value:auto-value:${autovalueVersion}", + autovalue_annotation: "com.google.auto.value:auto-value-annotations:${autovalueVersion}", errorprone: "com.google.errorprone:error_prone_annotations:2.4.0", cronet_api: 'org.chromium.net:cronet-api:76.3809.111', cronet_embedded: 'org.chromium.net:cronet-embedded:66.3359.158', diff --git a/xds/build.gradle b/xds/build.gradle index 132c1e5c8a..5e52988e37 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -13,7 +13,13 @@ description = "gRPC: XDS plugin" it.options.compilerArgs += [ // valueOf(int) in RoutingPriority has been deprecated "-Xlint:-deprecation", + // only has AutoValue annotation processor + "-Xlint:-processing", ] + appendToProperty( + it.options.errorprone.excludedPaths, + ".*/build/generated/sources/annotationProcessor/java/.*", + "|") } evaluationDependsOn(project(':grpc-core').path) @@ -27,7 +33,8 @@ dependencies { project(path: ':grpc-alts', configuration: 'shadow'), libraries.gson, libraries.re2j, - libraries.bouncycastle + libraries.bouncycastle, + libraries.autovalue_annotation def nettyDependency = implementation project(':grpc-netty') implementation (libraries.opencensus_proto) { @@ -44,6 +51,7 @@ dependencies { testImplementation project(':grpc-core').sourceSets.test.output + annotationProcessor libraries.autovalue compileOnly libraries.javax_annotation, // At runtime use the epoll included in grpc-netty-shaded libraries.netty_epoll diff --git a/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java index 7893cdca70..3a78172bf5 100644 --- a/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java @@ -27,7 +27,6 @@ import io.grpc.internal.GrpcUtil; import io.grpc.internal.GrpcUtil.GrpcBuildVersion; import io.grpc.internal.JsonParser; import io.grpc.internal.JsonUtil; -import io.grpc.xds.EnvoyProtoData.Locality; import io.grpc.xds.EnvoyProtoData.Node; import io.grpc.xds.XdsLogger.XdsLogLevel; import java.io.IOException; @@ -214,7 +213,7 @@ public class BootstrapperImpl implements Bootstrapper { if (rawLocality.containsKey("sub_zone")) { logger.log(XdsLogLevel.INFO, "Locality sub_zone: {0}", subZone); } - Locality locality = new Locality(region, zone, subZone); + Locality locality = Locality.create(region, zone, subZone); nodeBuilder.setLocality(locality); } } diff --git a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java index dee6191f3a..324c49d532 100644 --- a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java +++ b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java @@ -17,15 +17,18 @@ package io.grpc.xds; import static com.google.common.base.Preconditions.checkArgument; -import static io.grpc.xds.EnvoyProtoData.HTTP_FAULT_FILTER_NAME; +import static com.google.common.base.Preconditions.checkNotNull; import static io.grpc.xds.EnvoyProtoData.TRANSPORT_SOCKET_NAME_TLS; 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.protobuf.Any; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.util.Durations; +import com.google.re2j.Pattern; +import com.google.re2j.PatternSyntaxException; import io.envoyproxy.envoy.config.cluster.v3.CircuitBreakers.Thresholds; import io.envoyproxy.envoy.config.cluster.v3.Cluster; import io.envoyproxy.envoy.config.cluster.v3.Cluster.CustomClusterType; @@ -34,31 +37,41 @@ import io.envoyproxy.envoy.config.cluster.v3.Cluster.LbPolicy; import io.envoyproxy.envoy.config.core.v3.HttpProtocolOptions; import io.envoyproxy.envoy.config.core.v3.RoutingPriority; import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; -import io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint; import io.envoyproxy.envoy.config.listener.v3.Listener; import io.envoyproxy.envoy.config.route.v3.RouteConfiguration; -import io.envoyproxy.envoy.config.route.v3.VirtualHost; +import io.envoyproxy.envoy.extensions.filters.http.fault.v3.HTTPFault; import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager; import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter; import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.Rds; +import io.envoyproxy.envoy.type.v3.FractionalPercent; +import io.envoyproxy.envoy.type.v3.FractionalPercent.DenominatorType; +import io.grpc.EquivalentAddressGroup; import io.grpc.ManagedChannel; import io.grpc.Status; import io.grpc.SynchronizationContext.ScheduledHandle; import io.grpc.internal.BackoffPolicy; -import io.grpc.xds.EnvoyProtoData.DropOverload; -import io.grpc.xds.EnvoyProtoData.HttpFault; -import io.grpc.xds.EnvoyProtoData.Locality; -import io.grpc.xds.EnvoyProtoData.LocalityLbEndpoints; +import io.grpc.xds.Endpoints.DropOverload; +import io.grpc.xds.Endpoints.LbEndpoint; +import io.grpc.xds.Endpoints.LocalityLbEndpoints; import io.grpc.xds.EnvoyProtoData.Node; -import io.grpc.xds.EnvoyProtoData.StructOrError; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; +import io.grpc.xds.HttpFault.FaultAbort; +import io.grpc.xds.HttpFault.FaultDelay; import io.grpc.xds.LoadStatsManager2.ClusterDropStats; import io.grpc.xds.LoadStatsManager2.ClusterLocalityStats; +import io.grpc.xds.Matchers.FractionMatcher; +import io.grpc.xds.Matchers.HeaderMatcher; +import io.grpc.xds.Matchers.PathMatcher; +import io.grpc.xds.VirtualHost.Route; +import io.grpc.xds.VirtualHost.Route.RouteAction; +import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight; +import io.grpc.xds.VirtualHost.Route.RouteMatch; import io.grpc.xds.XdsClient.CdsUpdate.AggregateClusterConfig; import io.grpc.xds.XdsClient.CdsUpdate.ClusterType; import io.grpc.xds.XdsClient.CdsUpdate.EdsClusterConfig; import io.grpc.xds.XdsClient.CdsUpdate.LogicalDnsClusterConfig; import io.grpc.xds.XdsLogger.XdsLogLevel; +import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -82,6 +95,7 @@ final class ClientXdsClient extends AbstractXdsClient { static final int INITIAL_RESOURCE_FETCH_TIMEOUT_SEC = 15; @VisibleForTesting static final String AGGREGATE_CLUSTER_TYPE_NAME = "envoy.clusters.aggregate"; + private static final String HTTP_FAULT_FILTER_NAME = "envoy.fault"; private static final String TYPE_URL_HTTP_CONNECTION_MANAGER_V2 = "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2" + ".HttpConnectionManager"; @@ -176,7 +190,7 @@ final class ClientXdsClient extends AbstractXdsClient { hasFaultInjection = true; if (httpFilter.hasTypedConfig()) { StructOrError httpFaultOrError = - HttpFault.decodeFaultFilterConfig(httpFilter.getTypedConfig()); + decodeFaultFilterConfig(httpFilter.getTypedConfig()); if (httpFaultOrError.getErrorDetail() != null) { nackResponse(ResourceType.LDS, nonce, "Listener " + listenerName + " contains invalid HttpFault filter: " @@ -189,10 +203,10 @@ final class ClientXdsClient extends AbstractXdsClient { } } if (hcm.hasRouteConfig()) { - List virtualHosts = new ArrayList<>(); - for (VirtualHost virtualHostProto : hcm.getRouteConfig().getVirtualHostsList()) { - StructOrError virtualHost = - EnvoyProtoData.VirtualHost.fromEnvoyProtoVirtualHost(virtualHostProto); + List virtualHosts = new ArrayList<>(); + for (io.envoyproxy.envoy.config.route.v3.VirtualHost virtualHostProto + : hcm.getRouteConfig().getVirtualHostsList()) { + StructOrError virtualHost = parseVirtualHost(virtualHostProto); if (virtualHost.getErrorDetail() != null) { nackResponse(ResourceType.LDS, nonce, "Listener " + listenerName + " contains invalid virtual host: " @@ -237,6 +251,386 @@ final class ClientXdsClient extends AbstractXdsClient { } } + private static StructOrError parseVirtualHost( + io.envoyproxy.envoy.config.route.v3.VirtualHost proto) { + String name = proto.getName(); + List routes = new ArrayList<>(proto.getRoutesCount()); + for (io.envoyproxy.envoy.config.route.v3.Route routeProto : proto.getRoutesList()) { + StructOrError route = parseRoute(routeProto); + if (route == null) { + continue; + } + if (route.getErrorDetail() != null) { + return StructOrError.fromError( + "Virtual host [" + name + "] contains invalid route : " + route.getErrorDetail()); + } + routes.add(route.getStruct()); + } + HttpFault httpFault = null; + Map filterConfigMap = proto.getTypedPerFilterConfigMap(); + if (filterConfigMap.containsKey(HTTP_FAULT_FILTER_NAME)) { + Any rawFaultFilterConfig = filterConfigMap.get(HTTP_FAULT_FILTER_NAME); + StructOrError httpFaultOrError = decodeFaultFilterConfig(rawFaultFilterConfig); + if (httpFaultOrError.getErrorDetail() != null) { + return StructOrError.fromError( + "Virtual host [" + name + "] contains invalid HttpFault filter : " + + httpFaultOrError.getErrorDetail()); + } + httpFault = httpFaultOrError.getStruct(); + } + return StructOrError.fromStruct(VirtualHost.create( + name, proto.getDomainsList(), routes, httpFault)); + } + + @VisibleForTesting + @Nullable + static StructOrError parseRoute(io.envoyproxy.envoy.config.route.v3.Route proto) { + StructOrError routeMatch = parseRouteMatch(proto.getMatch()); + if (routeMatch == null) { + return null; + } + if (routeMatch.getErrorDetail() != null) { + return StructOrError.fromError( + "Invalid route [" + proto.getName() + "]: " + routeMatch.getErrorDetail()); + } + + StructOrError routeAction; + switch (proto.getActionCase()) { + case ROUTE: + routeAction = parseRouteAction(proto.getRoute()); + break; + case REDIRECT: + return StructOrError.fromError("Unsupported action type: redirect"); + case DIRECT_RESPONSE: + return StructOrError.fromError("Unsupported action type: direct_response"); + case FILTER_ACTION: + return StructOrError.fromError("Unsupported action type: filter_action"); + case ACTION_NOT_SET: + default: + return StructOrError.fromError("Unknown action type: " + proto.getActionCase()); + } + if (routeAction == null) { + return null; + } + if (routeAction.getErrorDetail() != null) { + return StructOrError.fromError( + "Invalid route [" + proto.getName() + "]: " + routeAction.getErrorDetail()); + } + + HttpFault httpFault = null; + Map filterConfigMap = proto.getTypedPerFilterConfigMap(); + if (filterConfigMap.containsKey(HTTP_FAULT_FILTER_NAME)) { + Any rawFaultFilterConfig = filterConfigMap.get(HTTP_FAULT_FILTER_NAME); + StructOrError httpFaultOrError = decodeFaultFilterConfig(rawFaultFilterConfig); + if (httpFaultOrError.getErrorDetail() != null) { + return StructOrError.fromError( + "Route [" + proto.getName() + "] contains invalid HttpFault filter: " + + httpFaultOrError.getErrorDetail()); + } + httpFault = httpFaultOrError.getStruct(); + } + return StructOrError.fromStruct(Route.create( + routeMatch.getStruct(), routeAction.getStruct(), httpFault)); + } + + @VisibleForTesting + @Nullable + static StructOrError parseRouteMatch( + io.envoyproxy.envoy.config.route.v3.RouteMatch proto) { + if (proto.getQueryParametersCount() != 0) { + return null; + } + StructOrError pathMatch = parsePathMatcher(proto); + if (pathMatch.getErrorDetail() != null) { + return StructOrError.fromError(pathMatch.getErrorDetail()); + } + + FractionMatcher fractionMatch = null; + if (proto.hasRuntimeFraction()) { + StructOrError parsedFraction = + parseFractionMatcher(proto.getRuntimeFraction().getDefaultValue()); + if (parsedFraction.getErrorDetail() != null) { + return StructOrError.fromError(parsedFraction.getErrorDetail()); + } + fractionMatch = parsedFraction.getStruct(); + } + + List headerMatchers = new ArrayList<>(); + for (io.envoyproxy.envoy.config.route.v3.HeaderMatcher hmProto : proto.getHeadersList()) { + StructOrError headerMatcher = parseHeaderMatcher(hmProto); + if (headerMatcher.getErrorDetail() != null) { + return StructOrError.fromError(headerMatcher.getErrorDetail()); + } + headerMatchers.add(headerMatcher.getStruct()); + } + + return StructOrError.fromStruct(RouteMatch.create( + pathMatch.getStruct(), headerMatchers, fractionMatch)); + } + + @VisibleForTesting + static StructOrError parsePathMatcher( + io.envoyproxy.envoy.config.route.v3.RouteMatch proto) { + boolean caseSensitive = proto.getCaseSensitive().getValue(); + switch (proto.getPathSpecifierCase()) { + case PREFIX: + return StructOrError.fromStruct( + PathMatcher.fromPrefix(proto.getPrefix(), caseSensitive)); + case PATH: + return StructOrError.fromStruct(PathMatcher.fromPath(proto.getPath(), caseSensitive)); + case SAFE_REGEX: + String rawPattern = proto.getSafeRegex().getRegex(); + Pattern safeRegEx; + try { + safeRegEx = Pattern.compile(rawPattern); + } catch (PatternSyntaxException e) { + return StructOrError.fromError("Malformed safe regex pattern: " + e.getMessage()); + } + return StructOrError.fromStruct(PathMatcher.fromRegEx(safeRegEx)); + case PATHSPECIFIER_NOT_SET: + default: + return StructOrError.fromError("Unknown path match type"); + } + } + + private static StructOrError parseFractionMatcher( + io.envoyproxy.envoy.type.v3.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(FractionMatcher.create(numerator, denominator)); + } + + @VisibleForTesting + static StructOrError parseHeaderMatcher( + io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto) { + switch (proto.getHeaderMatchSpecifierCase()) { + case EXACT_MATCH: + return StructOrError.fromStruct(HeaderMatcher.forExactValue( + proto.getName(), proto.getExactMatch(), proto.getInvertMatch())); + case SAFE_REGEX_MATCH: + String rawPattern = proto.getSafeRegexMatch().getRegex(); + Pattern safeRegExMatch; + try { + safeRegExMatch = Pattern.compile(rawPattern); + } catch (PatternSyntaxException e) { + return StructOrError.fromError( + "HeaderMatcher [" + proto.getName() + "] contains malformed safe regex pattern: " + + e.getMessage()); + } + return StructOrError.fromStruct(HeaderMatcher.forSafeRegEx( + proto.getName(), safeRegExMatch, proto.getInvertMatch())); + case RANGE_MATCH: + HeaderMatcher.Range rangeMatch = HeaderMatcher.Range.create( + proto.getRangeMatch().getStart(), proto.getRangeMatch().getEnd()); + return StructOrError.fromStruct(HeaderMatcher.forRange( + proto.getName(), rangeMatch, proto.getInvertMatch())); + case PRESENT_MATCH: + return StructOrError.fromStruct(HeaderMatcher.forPresent( + proto.getName(), proto.getPresentMatch(), proto.getInvertMatch())); + case PREFIX_MATCH: + return StructOrError.fromStruct(HeaderMatcher.forPrefix( + proto.getName(), proto.getPrefixMatch(), proto.getInvertMatch())); + case SUFFIX_MATCH: + return StructOrError.fromStruct(HeaderMatcher.forSuffix( + proto.getName(), proto.getSuffixMatch(), proto.getInvertMatch())); + case HEADERMATCHSPECIFIER_NOT_SET: + default: + return StructOrError.fromError("Unknown header matcher type"); + } + } + + @VisibleForTesting + @Nullable + static StructOrError parseRouteAction( + io.envoyproxy.envoy.config.route.v3.RouteAction proto) { + Long timeoutNano = null; + if (proto.hasMaxStreamDuration()) { + io.envoyproxy.envoy.config.route.v3.RouteAction.MaxStreamDuration maxStreamDuration + = proto.getMaxStreamDuration(); + if (maxStreamDuration.hasGrpcTimeoutHeaderMax()) { + timeoutNano = Durations.toNanos(maxStreamDuration.getGrpcTimeoutHeaderMax()); + } else if (maxStreamDuration.hasMaxStreamDuration()) { + timeoutNano = Durations.toNanos(maxStreamDuration.getMaxStreamDuration()); + } + } + List weightedClusters; + switch (proto.getClusterSpecifierCase()) { + case CLUSTER: + return StructOrError.fromStruct(RouteAction.forCluster(proto.getCluster(), timeoutNano)); + case CLUSTER_HEADER: + return null; + case WEIGHTED_CLUSTERS: + List clusterWeights + = proto.getWeightedClusters().getClustersList(); + if (clusterWeights.isEmpty()) { + return StructOrError.fromError("No cluster found in weighted cluster list"); + } + weightedClusters = new ArrayList<>(); + for (io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight clusterWeight + : clusterWeights) { + StructOrError clusterWeightOrError = parseClusterWeight(clusterWeight); + if (clusterWeightOrError.getErrorDetail() != null) { + return StructOrError.fromError("RouteAction contains invalid ClusterWeight: " + + clusterWeightOrError.getErrorDetail()); + } + weightedClusters.add(clusterWeightOrError.getStruct()); + } + // TODO(chengyuanzhang): validate if the sum of weights equals to total weight. + return StructOrError.fromStruct(RouteAction.forWeightedClusters( + weightedClusters, timeoutNano)); + case CLUSTERSPECIFIER_NOT_SET: + default: + return StructOrError.fromError( + "Unknown cluster specifier: " + proto.getClusterSpecifierCase()); + } + } + + @VisibleForTesting + static StructOrError parseClusterWeight( + io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight proto) { + HttpFault httpFault = null; + Map filterConfigMap = proto.getTypedPerFilterConfigMap(); + if (filterConfigMap.containsKey(HTTP_FAULT_FILTER_NAME)) { + Any rawFaultFilterConfig = filterConfigMap.get(HTTP_FAULT_FILTER_NAME); + StructOrError httpFaultOrError = decodeFaultFilterConfig(rawFaultFilterConfig); + if (httpFaultOrError.getErrorDetail() != null) { + return StructOrError.fromError( + "ClusterWeight [" + proto.getName() + "] contains invalid HttpFault filter: " + + httpFaultOrError.getErrorDetail()); + } + httpFault = httpFaultOrError.getStruct(); + } + return StructOrError.fromStruct( + ClusterWeight.create(proto.getName(), proto.getWeight().getValue(), httpFault)); + } + + private static StructOrError decodeFaultFilterConfig(Any rawFaultFilterConfig) { + if (rawFaultFilterConfig.getTypeUrl().equals( + "type.googleapis.com/envoy.config.filter.http.fault.v2.HTTPFault")) { + rawFaultFilterConfig = rawFaultFilterConfig.toBuilder().setTypeUrl( + "type.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault").build(); + } + HTTPFault httpFaultProto; + try { + httpFaultProto = rawFaultFilterConfig.unpack(HTTPFault.class); + } catch (InvalidProtocolBufferException e) { + return StructOrError.fromError("Invalid proto: " + e); + } + return parseHttpFault(httpFaultProto); + } + + private static StructOrError parseHttpFault(HTTPFault httpFault) { + FaultDelay faultDelay = null; + FaultAbort faultAbort = null; + if (httpFault.hasDelay()) { + faultDelay = parseFaultDelay(httpFault.getDelay()); + } + if (httpFault.hasAbort()) { + StructOrError faultAbortOrError = parseFaultAbort(httpFault.getAbort()); + if (faultAbortOrError.getErrorDetail() != null) { + return StructOrError.fromError( + "HttpFault contains invalid FaultAbort: " + faultAbortOrError.getErrorDetail()); + } + faultAbort = faultAbortOrError.getStruct(); + } + if (faultDelay == null && faultAbort == null) { + return StructOrError.fromError( + "Invalid HttpFault: neither fault_delay nor fault_abort is specified"); + } + String upstreamCluster = httpFault.getUpstreamCluster(); + List downstreamNodes = httpFault.getDownstreamNodesList(); + List headers = new ArrayList<>(); + for (io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto : httpFault.getHeadersList()) { + StructOrError headerMatcherOrError = parseHeaderMatcher(proto); + if (headerMatcherOrError.getErrorDetail() != null) { + return StructOrError.fromError( + "HttpFault contains invalid header matcher: " + + headerMatcherOrError.getErrorDetail()); + } + headers.add(headerMatcherOrError.getStruct()); + } + Integer maxActiveFaults = null; + if (httpFault.hasMaxActiveFaults()) { + maxActiveFaults = httpFault.getMaxActiveFaults().getValue(); + if (maxActiveFaults < 0) { + maxActiveFaults = Integer.MAX_VALUE; + } + } + return StructOrError.fromStruct(HttpFault.create( + faultDelay, faultAbort, upstreamCluster, downstreamNodes, headers, maxActiveFaults)); + } + + private static FaultDelay parseFaultDelay( + io.envoyproxy.envoy.extensions.filters.common.fault.v3.FaultDelay faultDelay) { + int rate = getRatePerMillion(faultDelay.getPercentage()); + if (faultDelay.hasHeaderDelay()) { + return FaultDelay.forHeader(rate); + } + return FaultDelay.forFixedDelay(Durations.toNanos(faultDelay.getFixedDelay()), rate); + } + + @VisibleForTesting + static StructOrError parseFaultAbort( + io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort faultAbort) { + int rate = getRatePerMillion(faultAbort.getPercentage()); + switch (faultAbort.getErrorTypeCase()) { + case HEADER_ABORT: + return StructOrError.fromStruct(FaultAbort.forHeader(rate)); + case HTTP_STATUS: + return StructOrError.fromStruct(FaultAbort.forStatus( + convertHttpStatus(faultAbort.getHttpStatus()), rate)); + case GRPC_STATUS: + return StructOrError.fromStruct(FaultAbort.forStatus( + Status.fromCodeValue(faultAbort.getGrpcStatus()), rate)); + case ERRORTYPE_NOT_SET: + default: + return StructOrError.fromError( + "Unknown error type case: " + faultAbort.getErrorTypeCase()); + } + } + + private static Status convertHttpStatus(int httpCode) { + Status status; + switch (httpCode) { + case 400: + status = Status.INTERNAL; + break; + case 401: + status = Status.UNAUTHENTICATED; + break; + case 403: + status = Status.PERMISSION_DENIED; + break; + case 404: + status = Status.UNIMPLEMENTED; + break; + case 429: + case 502: + case 503: + case 504: + status = Status.UNAVAILABLE; + break; + default: + status = Status.UNKNOWN; + } + return status.withDescription("HTTP code: " + httpCode); + } + @Override protected void handleRdsResponse(String versionInfo, List resources, String nonce) { // Unpack RouteConfiguration messages. @@ -262,11 +656,11 @@ final class ClientXdsClient extends AbstractXdsClient { for (Map.Entry entry : routeConfigs.entrySet()) { String routeConfigName = entry.getKey(); RouteConfiguration routeConfig = entry.getValue(); - List virtualHosts = + List virtualHosts = new ArrayList<>(routeConfig.getVirtualHostsCount()); - for (VirtualHost virtualHostProto : routeConfig.getVirtualHostsList()) { - StructOrError virtualHost = - EnvoyProtoData.VirtualHost.fromEnvoyProtoVirtualHost(virtualHostProto); + for (io.envoyproxy.envoy.config.route.v3.VirtualHost virtualHostProto + : routeConfig.getVirtualHostsList()) { + StructOrError virtualHost = parseVirtualHost(virtualHostProto); if (virtualHost.getErrorDetail() != null) { nackResponse(ResourceType.RDS, nonce, "RouteConfiguration " + routeConfigName + " contains invalid virtual host: " + virtualHost.getErrorDetail()); @@ -508,45 +902,35 @@ final class ClientXdsClient extends AbstractXdsClient { Map localityLbEndpointsMap = new LinkedHashMap<>(); List dropOverloads = new ArrayList<>(); int maxPriority = -1; - for (io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints localityLbEndpoints + for (io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints localityLbEndpointsProto : assignment.getEndpointsList()) { - // Filter out localities without or with 0 weight. - if (!localityLbEndpoints.hasLoadBalancingWeight() - || localityLbEndpoints.getLoadBalancingWeight().getValue() < 1) { + StructOrError localityLbEndpoints = + parseLocalityLbEndpoints(localityLbEndpointsProto); + if (localityLbEndpoints == null) { continue; } - int localityPriority = localityLbEndpoints.getPriority(); - if (localityPriority < 0) { - nackResponse(ResourceType.EDS, nonce, - "ClusterLoadAssignment " + clusterName + " : locality with negative priority."); + if (localityLbEndpoints.getErrorDetail() != null) { + nackResponse(ResourceType.EDS, nonce, "ClusterLoadAssignment " + clusterName + ": " + + localityLbEndpoints.getErrorDetail()); return; } - maxPriority = Math.max(maxPriority, localityPriority); - priorities.add(localityPriority); - // The endpoint field of each lb_endpoints must be set. - // Inside of it: the address field must be set. - for (LbEndpoint lbEndpoint : localityLbEndpoints.getLbEndpointsList()) { - if (!lbEndpoint.getEndpoint().hasAddress()) { - nackResponse(ResourceType.EDS, nonce, - "ClusterLoadAssignment " + clusterName + " : endpoint with no address."); - return; - } - } - // Note endpoints with health status other than UNHEALTHY and UNKNOWN are still + maxPriority = Math.max(maxPriority, localityLbEndpoints.getStruct().priority()); + priorities.add(localityLbEndpoints.getStruct().priority()); + // Note endpoints with health status other than HEALTHY and UNKNOWN are still // handed over to watching parties. It is watching parties' responsibility to // filter out unhealthy endpoints. See EnvoyProtoData.LbEndpoint#isHealthy(). localityLbEndpointsMap.put( - Locality.fromEnvoyProtoLocality(localityLbEndpoints.getLocality()), - LocalityLbEndpoints.fromEnvoyProtoLocalityLbEndpoints(localityLbEndpoints)); + parseLocality(localityLbEndpointsProto.getLocality()), + localityLbEndpoints.getStruct()); } if (priorities.size() != maxPriority + 1) { nackResponse(ResourceType.EDS, nonce, "ClusterLoadAssignment " + clusterName + " : sparse priorities."); return; } - for (ClusterLoadAssignment.Policy.DropOverload dropOverload + for (ClusterLoadAssignment.Policy.DropOverload dropOverloadProto : assignment.getPolicy().getDropOverloadsList()) { - dropOverloads.add(DropOverload.fromEnvoyProtoDropOverload(dropOverload)); + dropOverloads.add(parseDropOverload(dropOverloadProto)); } EdsUpdate update = new EdsUpdate(clusterName, localityLbEndpointsMap, dropOverloads); edsUpdates.put(clusterName, update); @@ -561,6 +945,72 @@ final class ClientXdsClient extends AbstractXdsClient { } } + private static Locality parseLocality(io.envoyproxy.envoy.config.core.v3.Locality proto) { + return Locality.create(proto.getRegion(), proto.getZone(), proto.getSubZone()); + } + + private static DropOverload parseDropOverload( + io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment.Policy.DropOverload proto) { + return DropOverload.create(proto.getCategory(), getRatePerMillion(proto.getDropPercentage())); + } + + @VisibleForTesting + @Nullable + static StructOrError parseLocalityLbEndpoints( + io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto) { + // Filter out localities without or with 0 weight. + if (!proto.hasLoadBalancingWeight() || proto.getLoadBalancingWeight().getValue() < 1) { + return null; + } + if (proto.getPriority() < 0) { + return StructOrError.fromError("negative priority"); + } + List endpoints = new ArrayList<>(proto.getLbEndpointsCount()); + for (io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint endpoint : proto.getLbEndpointsList()) { + // The endpoint field of each lb_endpoints must be set. + // Inside of it: the address field must be set. + if (!endpoint.hasEndpoint() || !endpoint.getEndpoint().hasAddress()) { + return StructOrError.fromError("LbEndpoint with no endpoint/address"); + } + io.envoyproxy.envoy.config.core.v3.SocketAddress socketAddress = + endpoint.getEndpoint().getAddress().getSocketAddress(); + InetSocketAddress addr = + new InetSocketAddress(socketAddress.getAddress(), socketAddress.getPortValue()); + boolean isHealthy = + endpoint.getHealthStatus() == io.envoyproxy.envoy.config.core.v3.HealthStatus.HEALTHY + || endpoint.getHealthStatus() + == io.envoyproxy.envoy.config.core.v3.HealthStatus.UNKNOWN; + endpoints.add(LbEndpoint.create( + new EquivalentAddressGroup(ImmutableList.of(addr)), + endpoint.getLoadBalancingWeight().getValue(), isHealthy)); + } + return StructOrError.fromStruct(LocalityLbEndpoints.create( + endpoints, proto.getLoadBalancingWeight().getValue(), proto.getPriority())); + } + + private static int getRatePerMillion(FractionalPercent percent) { + int numerator = percent.getNumerator(); + DenominatorType type = percent.getDenominator(); + switch (type) { + case TEN_THOUSAND: + numerator *= 100; + break; + case HUNDRED: + numerator *= 10_000; + break; + case MILLION: + break; + case UNRECOGNIZED: + default: + throw new IllegalArgumentException("Unknown denominator type of " + percent); + } + + if (numerator > 1_000_000 || numerator < 0) { + numerator = 1_000_000; + } + return numerator; + } + @Override protected void handleStreamClosed(Status error) { cleanUpResourceTimers(); @@ -933,4 +1383,53 @@ final class ClientXdsClient extends AbstractXdsClient { } } } + + @VisibleForTesting + static final class StructOrError { + + /** + * Returns a {@link StructOrError} for the successfully converted data object. + */ + private static StructOrError fromStruct(T struct) { + return new StructOrError<>(struct); + } + + /** + * Returns a {@link StructOrError} for the failure to convert the data object. + */ + private static StructOrError fromError(String errorDetail) { + return new StructOrError<>(errorDetail); + } + + private final String errorDetail; + private final T struct; + + private StructOrError(T struct) { + this.struct = checkNotNull(struct, "struct"); + this.errorDetail = null; + } + + private StructOrError(String errorDetail) { + this.struct = null; + this.errorDetail = checkNotNull(errorDetail, "errorDetail"); + } + + /** + * Returns struct if exists, otherwise null. + */ + @VisibleForTesting + @Nullable + T getStruct() { + return struct; + } + + /** + * Returns error detail if exists, otherwise null. + */ + @VisibleForTesting + @Nullable + String getErrorDetail() { + return errorDetail; + } + } } diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java index 452a9951b2..a405e74bca 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java @@ -34,8 +34,7 @@ import io.grpc.util.ForwardingClientStreamTracer; import io.grpc.util.ForwardingLoadBalancerHelper; import io.grpc.util.ForwardingSubchannel; import io.grpc.xds.ClusterImplLoadBalancerProvider.ClusterImplConfig; -import io.grpc.xds.EnvoyProtoData.DropOverload; -import io.grpc.xds.EnvoyProtoData.Locality; +import io.grpc.xds.Endpoints.DropOverload; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.LoadStatsManager2.ClusterDropStats; import io.grpc.xds.LoadStatsManager2.ClusterLocalityStats; @@ -213,7 +212,7 @@ final class ClusterImplLoadBalancer extends LoadBalancer { // attributes with its locality, including endpoints in LOGICAL_DNS clusters. // In case of not (which really shouldn't), loads are aggregated under an empty locality. if (locality == null) { - locality = new Locality("", "", ""); + locality = Locality.create("", "", ""); } final ClusterLocalityStats localityStats = xdsClient.addClusterLocalityStats( cluster, edsServiceName, locality); @@ -292,14 +291,14 @@ final class ClusterImplLoadBalancer extends LoadBalancer { public PickResult pickSubchannel(PickSubchannelArgs args) { for (DropOverload dropOverload : dropPolicies) { int rand = random.nextInt(1_000_000); - if (rand < dropOverload.getDropsPerMillion()) { + if (rand < dropOverload.dropsPerMillion()) { logger.log(XdsLogLevel.INFO, "Drop request with category: {0}", - dropOverload.getCategory()); + dropOverload.category()); if (dropStats != null) { - dropStats.recordDroppedRequest(dropOverload.getCategory()); + dropStats.recordDroppedRequest(dropOverload.category()); } return PickResult.withDrop( - Status.UNAVAILABLE.withDescription("Dropped: " + dropOverload.getCategory())); + Status.UNAVAILABLE.withDescription("Dropped: " + dropOverload.category())); } } final PickResult result = delegate.pickSubchannel(args); diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancerProvider.java index 366fafbc8a..939734fe4f 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancerProvider.java @@ -26,7 +26,7 @@ import io.grpc.LoadBalancerProvider; import io.grpc.LoadBalancerRegistry; import io.grpc.NameResolver.ConfigOrError; import io.grpc.internal.ServiceConfigUtil.PolicySelection; -import io.grpc.xds.EnvoyProtoData.DropOverload; +import io.grpc.xds.Endpoints.DropOverload; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import java.util.ArrayList; import java.util.Collections; diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java index f002d5f809..4ee0e773a2 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java @@ -43,10 +43,9 @@ import io.grpc.util.GracefulSwitchLoadBalancer; import io.grpc.xds.ClusterImplLoadBalancerProvider.ClusterImplConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig.DiscoveryMechanism; -import io.grpc.xds.EnvoyProtoData.DropOverload; -import io.grpc.xds.EnvoyProtoData.LbEndpoint; -import io.grpc.xds.EnvoyProtoData.Locality; -import io.grpc.xds.EnvoyProtoData.LocalityLbEndpoints; +import io.grpc.xds.Endpoints.DropOverload; +import io.grpc.xds.Endpoints.LbEndpoint; +import io.grpc.xds.Endpoints.LocalityLbEndpoints; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; @@ -78,7 +77,7 @@ import javax.annotation.Nullable; */ final class ClusterResolverLoadBalancer extends LoadBalancer { - private static final Locality LOGICAL_DNS_CLUSTER_LOCALITY = new Locality("", "", ""); + private static final Locality LOGICAL_DNS_CLUSTER_LOCALITY = Locality.create("", "", ""); private final XdsLogger logger; private final String authority; private final SynchronizationContext syncContext; @@ -385,16 +384,16 @@ final class ClusterResolverLoadBalancer extends LoadBalancer { Map> prioritizedLocalityWeights = new HashMap<>(); for (Locality locality : localityLbEndpoints.keySet()) { LocalityLbEndpoints localityLbInfo = localityLbEndpoints.get(locality); - int priority = localityLbInfo.getPriority(); + int priority = localityLbInfo.priority(); String priorityName = priorityName(name, priority); boolean discard = true; - for (LbEndpoint endpoint : localityLbInfo.getEndpoints()) { + for (LbEndpoint endpoint : localityLbInfo.endpoints()) { if (endpoint.isHealthy()) { discard = false; - Attributes attr = endpoint.getAddress().getAttributes().toBuilder() + Attributes attr = endpoint.eag().getAttributes().toBuilder() .set(InternalXdsAttributes.ATTR_LOCALITY, locality).build(); EquivalentAddressGroup eag = - new EquivalentAddressGroup(endpoint.getAddress().getAddresses(), attr); + new EquivalentAddressGroup(endpoint.eag().getAddresses(), attr); eag = AddressFilter.setPathFilter( eag, Arrays.asList(priorityName, localityName(locality))); addresses.add(eag); @@ -409,7 +408,7 @@ final class ClusterResolverLoadBalancer extends LoadBalancer { prioritizedLocalityWeights.put(priorityName, new HashMap()); } prioritizedLocalityWeights.get(priorityName).put( - locality, localityLbInfo.getLocalityWeight()); + locality, localityLbInfo.localityWeight()); } if (prioritizedLocalityWeights.isEmpty()) { // Will still update the result, as if the cluster resource is revoked. diff --git a/xds/src/main/java/io/grpc/xds/Endpoints.java b/xds/src/main/java/io/grpc/xds/Endpoints.java new file mode 100644 index 0000000000..5f871c9d26 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/Endpoints.java @@ -0,0 +1,86 @@ +/* + * Copyright 2021 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.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import io.grpc.EquivalentAddressGroup; +import java.net.InetSocketAddress; +import java.util.List; + +/** Locality and endpoint level load balancing configurations. */ +final class Endpoints { + private Endpoints() {} + + /** Represents a group of endpoints belong to a single locality. */ + @AutoValue + abstract static class LocalityLbEndpoints { + // Endpoints to be load balanced. + abstract ImmutableList endpoints(); + + // Locality's weight for inter-locality load balancing. + abstract int localityWeight(); + + // Locality's priority level. + abstract int priority(); + + static LocalityLbEndpoints create(List endpoints, int localityWeight, + int priority) { + return new AutoValue_Endpoints_LocalityLbEndpoints( + ImmutableList.copyOf(endpoints), localityWeight, priority); + } + } + + /** Represents a single endpoint to be load balanced. */ + @AutoValue + abstract static class LbEndpoint { + // The endpoint address to be connected to. + abstract EquivalentAddressGroup eag(); + + // Endpoint's wight for load balancing. + abstract int loadBalancingWeight(); + + // Whether the endpoint is healthy. + abstract boolean isHealthy(); + + static LbEndpoint create(EquivalentAddressGroup eag, int loadBalancingWeight, + boolean isHealthy) { + return new AutoValue_Endpoints_LbEndpoint(eag, loadBalancingWeight, isHealthy); + } + + // Only for testing. + @VisibleForTesting + static LbEndpoint create( + String address, int port, int loadBalancingWeight, boolean isHealthy) { + return LbEndpoint.create(new EquivalentAddressGroup(new InetSocketAddress(address, port)), + loadBalancingWeight, isHealthy); + } + } + + /** Represents a drop policy. */ + @AutoValue + abstract static class DropOverload { + abstract String category(); + + abstract int dropsPerMillion(); + + static DropOverload create(String category, int dropsPerMillion) { + return new AutoValue_Endpoints_DropOverload(category, dropsPerMillion); + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java b/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java index 72a4c61fc2..442e0629ee 100644 --- a/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java +++ b/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java @@ -20,28 +20,11 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; -import com.google.common.base.MoreObjects.ToStringHelper; -import com.google.common.collect.ImmutableList; -import com.google.protobuf.Any; -import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.ListValue; import com.google.protobuf.NullValue; import com.google.protobuf.Struct; import com.google.protobuf.Value; -import com.google.protobuf.util.Durations; -import com.google.re2j.Pattern; -import com.google.re2j.PatternSyntaxException; -import io.envoyproxy.envoy.extensions.filters.http.fault.v3.HTTPFault; -import io.envoyproxy.envoy.type.v3.FractionalPercent; -import io.envoyproxy.envoy.type.v3.FractionalPercent.DenominatorType; -import io.grpc.EquivalentAddressGroup; -import io.grpc.Status; -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.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -65,89 +48,11 @@ import javax.annotation.Nullable; // TODO(chengyuanzhang): put data types into smaller categories. final class EnvoyProtoData { static final String TRANSPORT_SOCKET_NAME_TLS = "envoy.transport_sockets.tls"; - static final String HTTP_FAULT_FILTER_NAME = "envoy.fault"; // Prevent instantiation. private EnvoyProtoData() { } - static final class StructOrError { - - /** - * Returns a {@link StructOrError} for the successfully converted data object. - */ - static StructOrError fromStruct(T struct) { - return new StructOrError<>(struct); - } - - /** - * Returns a {@link StructOrError} for the failure to convert the data object. - */ - static StructOrError fromError(String errorDetail) { - return new StructOrError<>(errorDetail); - } - - private final String errorDetail; - private final T struct; - - private StructOrError(T struct) { - this.struct = checkNotNull(struct, "struct"); - this.errorDetail = null; - } - - private StructOrError(String errorDetail) { - this.struct = null; - this.errorDetail = checkNotNull(errorDetail, "errorDetail"); - } - - /** - * Returns struct if exists, otherwise null. - */ - @Nullable - public T getStruct() { - return struct; - } - - /** - * Returns error detail if exists, otherwise null. - */ - @Nullable - String getErrorDetail() { - return errorDetail; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - StructOrError that = (StructOrError) o; - return Objects.equals(errorDetail, that.errorDetail) && Objects.equals(struct, that.struct); - } - - @Override - public int hashCode() { - return Objects.hash(errorDetail, struct); - } - - @Override - public String toString() { - if (struct != null) { - return MoreObjects.toStringHelper(this) - .add("struct", struct) - .toString(); - } else { - assert errorDetail != null; - return MoreObjects.toStringHelper(this) - .add("error", errorDetail) - .toString(); - } - } - } - /** * See corresponding Envoy proto message {@link io.envoyproxy.envoy.config.core.v3.Node}. */ @@ -348,7 +253,11 @@ final class EnvoyProtoData { builder.setMetadata(structBuilder); } if (locality != null) { - builder.setLocality(locality.toEnvoyProtoLocality()); + builder.setLocality( + io.envoyproxy.envoy.config.core.v3.Locality.newBuilder() + .setRegion(locality.region()) + .setZone(locality.zone()) + .setSubZone(locality.subZone())); } for (Address address : listeningAddresses) { builder.addListeningAddresses(address.toEnvoyProtoAddress()); @@ -375,7 +284,11 @@ final class EnvoyProtoData { builder.setMetadata(structBuilder); } if (locality != null) { - builder.setLocality(locality.toEnvoyProtoLocalityV2()); + builder.setLocality( + io.envoyproxy.envoy.api.v2.core.Locality.newBuilder() + .setRegion(locality.region()) + .setZone(locality.zone()) + .setSubZone(locality.subZone())); } for (Address address : listeningAddresses) { builder.addListeningAddresses(address.toEnvoyProtoAddressV2()); @@ -478,1675 +391,4 @@ final class EnvoyProtoData { return Objects.hash(address, port); } } - - /** - * See corresponding Envoy proto message {@link io.envoyproxy.envoy.config.core.v3.Locality}. - */ - static final class Locality { - private final String region; - private final String zone; - private final String subZone; - - Locality(@Nullable String region, @Nullable String zone, @Nullable String subZone) { - this.region = region == null ? "" : region; - this.zone = zone == null ? "" : zone; - this.subZone = subZone == null ? "" : subZone; - } - - static Locality fromEnvoyProtoLocality(io.envoyproxy.envoy.config.core.v3.Locality locality) { - return new Locality( - /* region = */ locality.getRegion(), - /* zone = */ locality.getZone(), - /* subZone = */ locality.getSubZone()); - } - - @VisibleForTesting - static Locality fromEnvoyProtoLocalityV2(io.envoyproxy.envoy.api.v2.core.Locality locality) { - return new Locality( - /* region = */ locality.getRegion(), - /* zone = */ locality.getZone(), - /* subZone = */ locality.getSubZone()); - } - - io.envoyproxy.envoy.config.core.v3.Locality toEnvoyProtoLocality() { - return io.envoyproxy.envoy.config.core.v3.Locality.newBuilder() - .setRegion(region) - .setZone(zone) - .setSubZone(subZone) - .build(); - } - - io.envoyproxy.envoy.api.v2.core.Locality toEnvoyProtoLocalityV2() { - return io.envoyproxy.envoy.api.v2.core.Locality.newBuilder() - .setRegion(region) - .setZone(zone) - .setSubZone(subZone) - .build(); - } - - String getRegion() { - return region; - } - - String getZone() { - return zone; - } - - String getSubZone() { - return subZone; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Locality locality = (Locality) o; - return Objects.equals(region, locality.region) - && Objects.equals(zone, locality.zone) - && Objects.equals(subZone, locality.subZone); - } - - @Override - public int hashCode() { - return Objects.hash(region, zone, subZone); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("region", region) - .add("zone", zone) - .add("subZone", subZone) - .toString(); - } - } - - /** - * See corresponding Envoy proto message {@link - * io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints}. - */ - static final class LocalityLbEndpoints { - private final List endpoints; - private final int localityWeight; - private final int priority; - - /** Must only be used for testing. */ - @VisibleForTesting - LocalityLbEndpoints(List endpoints, int localityWeight, int priority) { - this.endpoints = endpoints; - this.localityWeight = localityWeight; - this.priority = priority; - } - - static LocalityLbEndpoints fromEnvoyProtoLocalityLbEndpoints( - io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto) { - List endpoints = new ArrayList<>(proto.getLbEndpointsCount()); - for (io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint endpoint : - proto.getLbEndpointsList()) { - endpoints.add(LbEndpoint.fromEnvoyProtoLbEndpoint(endpoint)); - } - return - new LocalityLbEndpoints( - endpoints, - proto.getLoadBalancingWeight().getValue(), - proto.getPriority()); - } - - @VisibleForTesting - static LocalityLbEndpoints fromEnvoyProtoLocalityLbEndpointsV2( - io.envoyproxy.envoy.api.v2.endpoint.LocalityLbEndpoints proto) { - List endpoints = new ArrayList<>(proto.getLbEndpointsCount()); - for (io.envoyproxy.envoy.api.v2.endpoint.LbEndpoint endpoint : proto.getLbEndpointsList()) { - endpoints.add(LbEndpoint.fromEnvoyProtoLbEndpointV2(endpoint)); - } - return - new LocalityLbEndpoints( - endpoints, - proto.getLoadBalancingWeight().getValue(), - proto.getPriority()); - } - - List getEndpoints() { - return Collections.unmodifiableList(endpoints); - } - - int getLocalityWeight() { - return localityWeight; - } - - int getPriority() { - return priority; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - LocalityLbEndpoints that = (LocalityLbEndpoints) o; - return localityWeight == that.localityWeight - && priority == that.priority - && Objects.equals(endpoints, that.endpoints); - } - - @Override - public int hashCode() { - return Objects.hash(endpoints, localityWeight, priority); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("endpoints", endpoints) - .add("localityWeight", localityWeight) - .add("priority", priority) - .toString(); - } - } - - /** - * See corresponding Envoy proto message - * {@link io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint}. - */ - static final class LbEndpoint { - private final EquivalentAddressGroup eag; - private final int loadBalancingWeight; - private final boolean isHealthy; - - @VisibleForTesting - LbEndpoint(String address, int port, int loadBalancingWeight, boolean isHealthy) { - this( - new EquivalentAddressGroup( - new InetSocketAddress(address, port)), - loadBalancingWeight, isHealthy); - } - - @VisibleForTesting - LbEndpoint(EquivalentAddressGroup eag, int loadBalancingWeight, boolean isHealthy) { - this.eag = eag; - this.loadBalancingWeight = loadBalancingWeight; - this.isHealthy = isHealthy; - } - - static LbEndpoint fromEnvoyProtoLbEndpoint( - io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint proto) { - io.envoyproxy.envoy.config.core.v3.SocketAddress socketAddress = - proto.getEndpoint().getAddress().getSocketAddress(); - InetSocketAddress addr = - new InetSocketAddress(socketAddress.getAddress(), socketAddress.getPortValue()); - return new LbEndpoint( - new EquivalentAddressGroup(ImmutableList.of(addr)), - proto.getLoadBalancingWeight().getValue(), - proto.getHealthStatus() == io.envoyproxy.envoy.config.core.v3.HealthStatus.HEALTHY - || proto.getHealthStatus() - == io.envoyproxy.envoy.config.core.v3.HealthStatus.UNKNOWN); - } - - private static LbEndpoint fromEnvoyProtoLbEndpointV2( - io.envoyproxy.envoy.api.v2.endpoint.LbEndpoint proto) { - io.envoyproxy.envoy.api.v2.core.SocketAddress socketAddress = - proto.getEndpoint().getAddress().getSocketAddress(); - InetSocketAddress addr = - new InetSocketAddress(socketAddress.getAddress(), socketAddress.getPortValue()); - return new LbEndpoint( - new EquivalentAddressGroup(ImmutableList.of(addr)), - proto.getLoadBalancingWeight().getValue(), - proto.getHealthStatus() == io.envoyproxy.envoy.api.v2.core.HealthStatus.HEALTHY - || proto.getHealthStatus() == io.envoyproxy.envoy.api.v2.core.HealthStatus.UNKNOWN); - } - - EquivalentAddressGroup getAddress() { - return eag; - } - - int getLoadBalancingWeight() { - return loadBalancingWeight; - } - - boolean isHealthy() { - return isHealthy; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - LbEndpoint that = (LbEndpoint) o; - return loadBalancingWeight == that.loadBalancingWeight - && Objects.equals(eag, that.eag) - && isHealthy == that.isHealthy; - } - - @Override - public int hashCode() { - return Objects.hash(eag, loadBalancingWeight, isHealthy); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("eag", eag) - .add("loadBalancingWeight", loadBalancingWeight) - .add("isHealthy", isHealthy) - .toString(); - } - } - - /** - * See corresponding Envoy proto message {@link - * io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment.Policy.DropOverload}. - */ - static final class DropOverload { - private final String category; - private final int dropsPerMillion; - - /** Must only be used for testing. */ - @VisibleForTesting - DropOverload(String category, int dropsPerMillion) { - this.category = category; - this.dropsPerMillion = dropsPerMillion; - } - - static DropOverload fromEnvoyProtoDropOverload( - io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment.Policy.DropOverload proto) { - FractionalPercent percent = proto.getDropPercentage(); - return new DropOverload(proto.getCategory(), getRatePerMillion(percent)); - } - - String getCategory() { - return category; - } - - int getDropsPerMillion() { - return dropsPerMillion; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - DropOverload that = (DropOverload) o; - return dropsPerMillion == that.dropsPerMillion && Objects.equals(category, that.category); - } - - @Override - public int hashCode() { - return Objects.hash(category, dropsPerMillion); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("category", category) - .add("dropsPerMillion", dropsPerMillion) - .toString(); - } - } - - /** See corresponding Envoy proto message {@link - * io.envoyproxy.envoy.config.route.v3.VirtualHost}. */ - static final class VirtualHost { - // Canonical name of this virtual host. - private final String name; - // A list of domains (host/authority header) that will be matched to this virtual host. - private final List domains; - // The list of routes that will be matched, in order, for incoming requests. - private final List routes; - @Nullable - private final HttpFault httpFault; - - @VisibleForTesting - VirtualHost(String name, List domains, List routes) { - this(name, domains, routes, null); - } - - VirtualHost(String name, List domains, List routes, HttpFault httpFault) { - this.name = name; - this.domains = domains; - this.routes = routes; - this.httpFault = httpFault; - } - - String getName() { - return name; - } - - List getDomains() { - return domains; - } - - List getRoutes() { - return routes; - } - - @Nullable - HttpFault getHttpFault() { - return httpFault; - } - - @Override - public String toString() { - ToStringHelper toStringHelper = MoreObjects.toStringHelper(this) - .add("name", name) - .add("domains", domains) - .add("routes", routes); - if (httpFault != null) { - toStringHelper.add("httpFault", httpFault); - } - return toStringHelper.toString(); - } - - static StructOrError fromEnvoyProtoVirtualHost( - io.envoyproxy.envoy.config.route.v3.VirtualHost proto) { - String name = proto.getName(); - List routes = new ArrayList<>(proto.getRoutesCount()); - for (io.envoyproxy.envoy.config.route.v3.Route routeProto : proto.getRoutesList()) { - StructOrError route = Route.fromEnvoyProtoRoute(routeProto); - if (route == null) { - continue; - } - if (route.getErrorDetail() != null) { - return StructOrError.fromError( - "Virtual host [" + name + "] contains invalid route : " + route.getErrorDetail()); - } - routes.add(route.getStruct()); - } - HttpFault httpFault = null; - Map filterConfigMap = proto.getTypedPerFilterConfigMap(); - if (filterConfigMap.containsKey(HTTP_FAULT_FILTER_NAME)) { - Any rawFaultFilterConfig = filterConfigMap.get(HTTP_FAULT_FILTER_NAME); - StructOrError httpFaultOrError = - HttpFault.decodeFaultFilterConfig(rawFaultFilterConfig); - if (httpFaultOrError.getErrorDetail() != null) { - return StructOrError.fromError( - "Virtual host [" + name + "] contains invalid HttpFault filter : " - + httpFaultOrError.getErrorDetail()); - } - httpFault = httpFaultOrError.getStruct(); - } - return StructOrError.fromStruct( - new VirtualHost( - name, Collections.unmodifiableList(proto.getDomainsList()), - Collections.unmodifiableList(routes), - httpFault)); - } - } - - /** See corresponding Envoy proto message {@link io.envoyproxy.envoy.config.route.v3.Route}. */ - static final class Route { - private final RouteMatch routeMatch; - private final RouteAction routeAction; - @Nullable - private final HttpFault httpFault; - - @VisibleForTesting - Route(RouteMatch routeMatch, RouteAction routeAction) { - this(routeMatch, routeAction, null); - } - - Route(RouteMatch routeMatch, RouteAction routeAction, @Nullable HttpFault httpFault) { - this.routeMatch = routeMatch; - this.routeAction = routeAction; - this.httpFault = httpFault; - } - - RouteMatch getRouteMatch() { - return routeMatch; - } - - RouteAction getRouteAction() { - return routeAction; - } - - @Nullable - HttpFault getHttpFault() { - return httpFault; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Route route = (Route) o; - return Objects.equals(routeMatch, route.routeMatch) - && Objects.equals(routeAction, route.routeAction) - && Objects.equals(httpFault, route.httpFault); - } - - @Override - public int hashCode() { - return Objects.hash(routeMatch, routeAction, httpFault); - } - - @Override - public String toString() { - ToStringHelper toStringHelper = MoreObjects.toStringHelper(this) - .add("routeMatch", routeMatch) - .add("routeAction", routeAction); - if (httpFault != null) { - toStringHelper.add("httpFault", httpFault); - } - return toStringHelper.toString(); - } - - @Nullable - static StructOrError fromEnvoyProtoRoute( - io.envoyproxy.envoy.config.route.v3.Route proto) { - StructOrError routeMatch = convertEnvoyProtoRouteMatch(proto.getMatch()); - if (routeMatch == null) { - return null; - } - if (routeMatch.getErrorDetail() != null) { - return StructOrError.fromError( - "Invalid route [" + proto.getName() + "]: " + routeMatch.getErrorDetail()); - } - - StructOrError routeAction; - switch (proto.getActionCase()) { - case ROUTE: - routeAction = RouteAction.fromEnvoyProtoRouteAction(proto.getRoute()); - break; - case REDIRECT: - return StructOrError.fromError("Unsupported action type: redirect"); - case DIRECT_RESPONSE: - return StructOrError.fromError("Unsupported action type: direct_response"); - case FILTER_ACTION: - return StructOrError.fromError("Unsupported action type: filter_action"); - case ACTION_NOT_SET: - default: - return StructOrError.fromError("Unknown action type: " + proto.getActionCase()); - } - if (routeAction == null) { - return null; - } - if (routeAction.getErrorDetail() != null) { - return StructOrError.fromError( - "Invalid route [" + proto.getName() + "]: " + routeAction.getErrorDetail()); - } - - HttpFault httpFault = null; - Map filterConfigMap = proto.getTypedPerFilterConfigMap(); - if (filterConfigMap.containsKey(HTTP_FAULT_FILTER_NAME)) { - Any rawFaultFilterConfig = filterConfigMap.get(HTTP_FAULT_FILTER_NAME); - StructOrError httpFaultOrError = - HttpFault.decodeFaultFilterConfig(rawFaultFilterConfig); - if (httpFaultOrError.getErrorDetail() != null) { - return StructOrError.fromError( - "Route [" + proto.getName() + "] contains invalid HttpFault filter: " - + httpFaultOrError.getErrorDetail()); - } - httpFault = httpFaultOrError.getStruct(); - } - return StructOrError.fromStruct( - new Route(routeMatch.getStruct(), routeAction.getStruct(), httpFault)); - } - - @VisibleForTesting - @Nullable - static StructOrError convertEnvoyProtoRouteMatch( - io.envoyproxy.envoy.config.route.v3.RouteMatch proto) { - if (proto.getQueryParametersCount() != 0) { - return null; - } - StructOrError pathMatch = convertEnvoyProtoPathMatcher(proto); - if (pathMatch.getErrorDetail() != null) { - return StructOrError.fromError(pathMatch.getErrorDetail()); - } - - 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.config.route.v3.HeaderMatcher hmProto : proto.getHeadersList()) { - StructOrError headerMatcher = convertEnvoyProtoHeaderMatcher(hmProto); - if (headerMatcher.getErrorDetail() != null) { - return StructOrError.fromError(headerMatcher.getErrorDetail()); - } - headerMatchers.add(headerMatcher.getStruct()); - } - - return StructOrError.fromStruct( - new RouteMatch( - pathMatch.getStruct(), Collections.unmodifiableList(headerMatchers), fractionMatch)); - } - - private static StructOrError convertEnvoyProtoPathMatcher( - io.envoyproxy.envoy.config.route.v3.RouteMatch proto) { - boolean caseSensitive = proto.getCaseSensitive().getValue(); - switch (proto.getPathSpecifierCase()) { - case PREFIX: - return StructOrError.fromStruct( - PathMatcher.fromPrefix(proto.getPrefix(), caseSensitive)); - case PATH: - return StructOrError.fromStruct(PathMatcher.fromPath(proto.getPath(), caseSensitive)); - case SAFE_REGEX: - String rawPattern = proto.getSafeRegex().getRegex(); - Pattern safeRegEx; - try { - safeRegEx = Pattern.compile(rawPattern); - } catch (PatternSyntaxException e) { - return StructOrError.fromError("Malformed safe regex pattern: " + e.getMessage()); - } - return StructOrError.fromStruct(PathMatcher.fromRegEx(safeRegEx)); - case PATHSPECIFIER_NOT_SET: - default: - return StructOrError.fromError("Unknown path match type"); - } - } - - private static StructOrError convertEnvoyProtoFraction( - io.envoyproxy.envoy.type.v3.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 convertEnvoyProtoHeaderMatcher( - io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto) { - String exactMatch = null; - Pattern safeRegExMatch = null; - HeaderMatcher.Range rangeMatch = null; - Boolean presentMatch = null; - String prefixMatch = null; - String suffixMatch = null; - - switch (proto.getHeaderMatchSpecifierCase()) { - case EXACT_MATCH: - exactMatch = proto.getExactMatch(); - break; - case SAFE_REGEX_MATCH: - String rawPattern = proto.getSafeRegexMatch().getRegex(); - try { - safeRegExMatch = Pattern.compile(rawPattern); - } catch (PatternSyntaxException e) { - return StructOrError.fromError( - "HeaderMatcher [" + proto.getName() + "] contains malformed safe regex pattern: " - + e.getMessage()); - } - break; - case RANGE_MATCH: - rangeMatch = - new HeaderMatcher.Range( - proto.getRangeMatch().getStart(), proto.getRangeMatch().getEnd()); - break; - case PRESENT_MATCH: - presentMatch = proto.getPresentMatch(); - break; - case PREFIX_MATCH: - prefixMatch = proto.getPrefixMatch(); - break; - case SUFFIX_MATCH: - suffixMatch = proto.getSuffixMatch(); - break; - case HEADERMATCHSPECIFIER_NOT_SET: - default: - return StructOrError.fromError("Unknown header matcher type"); - } - return StructOrError.fromStruct( - new HeaderMatcher( - proto.getName(), exactMatch, safeRegExMatch, rangeMatch, presentMatch, - prefixMatch, suffixMatch, proto.getInvertMatch())); - } - } - - /** - * See corresponding Envoy proto message {@link - * io.envoyproxy.envoy.extensions.filters.http.fault.v3.HTTPFault}. - */ - static final class HttpFault { - @Nullable - final FaultDelay faultDelay; - @Nullable - final FaultAbort faultAbort; - String upstreamCluster; - List downstreamNodes; - List headers; - @Nullable - Integer maxActiveFaults; - - private HttpFault( - @Nullable FaultDelay faultDelay, @Nullable FaultAbort faultAbort, String upstreamCluster, - List downstreamNodes, List headers, - @Nullable Integer maxActiveFaults) { - this.faultDelay = faultDelay; - this.faultAbort = faultAbort; - this.upstreamCluster = upstreamCluster; - this.downstreamNodes = downstreamNodes; - this.headers = headers; - this.maxActiveFaults = maxActiveFaults; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - HttpFault httpFault = (HttpFault) o; - return Objects.equals(faultDelay, httpFault.faultDelay) - && Objects.equals(faultAbort, httpFault.faultAbort) - && Objects.equals(upstreamCluster, httpFault.upstreamCluster) - && Objects.equals(downstreamNodes, httpFault.downstreamNodes) - && Objects.equals(headers, httpFault.headers) - && Objects.equals(maxActiveFaults, httpFault.maxActiveFaults); - } - - @Override - public int hashCode() { - return Objects.hash( - faultDelay, faultAbort, upstreamCluster, downstreamNodes, headers, maxActiveFaults); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("faultDelay", faultDelay) - .add("faultAbort", faultAbort) - .add("upstreamCluster", upstreamCluster) - .add("downstreamNodes", downstreamNodes) - .add("headers", headers) - .add("maxActiveFaults", maxActiveFaults) - .toString(); - } - - static StructOrError decodeFaultFilterConfig(Any rawFaultFilterConfig) { - if (rawFaultFilterConfig.getTypeUrl().equals( - "type.googleapis.com/envoy.config.filter.http.fault.v2.HTTPFault")) { - rawFaultFilterConfig = rawFaultFilterConfig.toBuilder().setTypeUrl( - "type.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault").build(); - } - HTTPFault httpFaultProto; - try { - httpFaultProto = rawFaultFilterConfig.unpack(HTTPFault.class); - } catch (InvalidProtocolBufferException e) { - return StructOrError.fromError("Invalid proto: " + e); - } - return fromEnvoyProtoHttpFault(httpFaultProto); - } - - private static StructOrError fromEnvoyProtoHttpFault(HTTPFault httpFault) { - FaultDelay faultDelay = null; - FaultAbort faultAbort = null; - if (httpFault.hasDelay()) { - faultDelay = FaultDelay.fromEnvoyProtoFaultDelay(httpFault.getDelay()); - } - if (httpFault.hasAbort()) { - StructOrError faultAbortOrError = - FaultAbort.fromEnvoyProtoFaultAbort(httpFault.getAbort()); - if (faultAbortOrError.getErrorDetail() != null) { - return StructOrError.fromError( - "HttpFault contains invalid FaultAbort: " + faultAbortOrError.getErrorDetail()); - } - faultAbort = faultAbortOrError.getStruct(); - } - if (faultDelay == null && faultAbort == null) { - return StructOrError.fromError( - "Invalid HttpFault: neither fault_delay nor fault_abort is specified"); - } - String upstreamCluster = httpFault.getUpstreamCluster(); - List downstreamNodes = httpFault.getDownstreamNodesList(); - List headers = new ArrayList<>(); - for (io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto : httpFault.getHeadersList()) { - StructOrError headerMatcherOrError = - Route.convertEnvoyProtoHeaderMatcher(proto); - if (headerMatcherOrError.getErrorDetail() != null) { - return StructOrError.fromError( - "HttpFault contains invalid header matcher: " - + headerMatcherOrError.getErrorDetail()); - } - headers.add(headerMatcherOrError.getStruct()); - } - Integer maxActiveFaults = null; - if (httpFault.hasMaxActiveFaults()) { - maxActiveFaults = httpFault.getMaxActiveFaults().getValue(); - if (maxActiveFaults < 0) { - maxActiveFaults = Integer.MAX_VALUE; - } - } - return StructOrError.fromStruct(new HttpFault( - faultDelay, faultAbort, upstreamCluster, downstreamNodes, headers, maxActiveFaults)); - } - } - - /** - * See corresponding Envoy proto message {@link - * io.envoyproxy.envoy.extensions.filters.common.fault.v3.FaultDelay}. - */ - static final class FaultDelay { - @Nullable - final Long delayNanos; - final boolean headerDelay; - final int ratePerMillion; - - private FaultDelay(@Nullable Long delayNanos, boolean headerDelay, int ratePerMillion) { - this.delayNanos = delayNanos; - this.headerDelay = headerDelay; - this.ratePerMillion = ratePerMillion; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - FaultDelay that = (FaultDelay) o; - return ratePerMillion == that.ratePerMillion - && headerDelay == that.headerDelay - && Objects.equals(delayNanos, that.delayNanos); - } - - @Override - public int hashCode() { - return Objects.hash(delayNanos, headerDelay, ratePerMillion); - } - - @Override - public String toString() { - ToStringHelper toStringHelper = MoreObjects.toStringHelper(this) - .add("ratePerMillion", ratePerMillion); - if (headerDelay) { - toStringHelper.add("type", "header delay"); - } else { - toStringHelper.add("type", "fixed delay"); - toStringHelper.add("delayNanos", delayNanos); - } - return toStringHelper.toString(); - } - - private static FaultDelay fromEnvoyProtoFaultDelay( - io.envoyproxy.envoy.extensions.filters.common.fault.v3.FaultDelay faultDelay) { - int rate = getRatePerMillion(faultDelay.getPercentage()); - if (faultDelay.hasHeaderDelay()) { - return new FaultDelay(null, true, rate); - } - long delay = Durations.toNanos(faultDelay.getFixedDelay()); - return new FaultDelay(delay, false, rate); - } - } - - /** - * See corresponding Envoy proto message {@link - * io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort}. - */ - static final class FaultAbort { - @Nullable - final Status status; - final boolean headerAbort; - final int ratePerMillion; - - private FaultAbort(@Nullable Status status, boolean headerAbort, int ratePerMillion) { - this.status = status; - this.headerAbort = headerAbort; - this.ratePerMillion = ratePerMillion; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - FaultAbort that = (FaultAbort) o; - return ratePerMillion == that.ratePerMillion - && headerAbort == that.headerAbort - && Objects.equals(status, that.status); - } - - @Override - public int hashCode() { - return Objects.hash(status, headerAbort, ratePerMillion); - } - - @Override - public String toString() { - ToStringHelper toStringHelper = MoreObjects.toStringHelper(this) - .add("ratePerMillion", ratePerMillion); - if (headerAbort) { - toStringHelper.add("type", "header abort"); - } else { - toStringHelper.add("type", "fixed status"); - toStringHelper.add("status", status); - } - return toStringHelper.toString(); - } - - private static StructOrError fromEnvoyProtoFaultAbort( - io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort faultAbort) { - int rate = getRatePerMillion(faultAbort.getPercentage()); - boolean headerAbort = false; - Status status = null; - switch (faultAbort.getErrorTypeCase()) { - case HEADER_ABORT: - headerAbort = true; - break; - case HTTP_STATUS: - status = convertHttpStatus(faultAbort.getHttpStatus()); - break; - case GRPC_STATUS: - status = Status.fromCodeValue(faultAbort.getGrpcStatus()); - break; - case ERRORTYPE_NOT_SET: - default: - return StructOrError.fromError( - "Unknown error type case: " + faultAbort.getErrorTypeCase()); - } - return StructOrError.fromStruct(new FaultAbort(status, headerAbort, rate)); - } - - private static Status convertHttpStatus(int httpCode) { - Status status; - switch (httpCode) { - case 400: - status = Status.INTERNAL; - break; - case 401: - status = Status.UNAUTHENTICATED; - break; - case 403: - status = Status.PERMISSION_DENIED; - break; - case 404: - status = Status.UNIMPLEMENTED; - break; - case 429: - case 502: - case 503: - case 504: - status = Status.UNAVAILABLE; - break; - default: - status = Status.UNKNOWN; - } - return status.withDescription("HTTP code: " + httpCode); - } - } - - private static int getRatePerMillion(FractionalPercent percent) { - int numerator = percent.getNumerator(); - DenominatorType type = percent.getDenominator(); - switch (type) { - case TEN_THOUSAND: - numerator *= 100; - break; - case HUNDRED: - numerator *= 10_000; - break; - case MILLION: - break; - case UNRECOGNIZED: - default: - throw new IllegalArgumentException("Unknown denominator type of " + percent); - } - - if (numerator > 1_000_000 || numerator < 0) { - numerator = 1_000_000; - } - return numerator; - } - - /** - * See corresponding Envoy proto message {@link io.envoyproxy.envoy.config.route.v3.RouteAction}. - */ - static final class RouteAction { - @Nullable - private final Long timeoutNano; - // Exactly one of the following fields is non-null. - @Nullable - private final String cluster; - @Nullable - private final List weightedClusters; - - @VisibleForTesting - RouteAction(@Nullable Long timeoutNano, @Nullable String cluster, - @Nullable List weightedClusters) { - this.timeoutNano = timeoutNano; - this.cluster = cluster; - this.weightedClusters = weightedClusters; - } - - @Nullable - Long getTimeoutNano() { - return timeoutNano; - } - - @Nullable - String getCluster() { - return cluster; - } - - @Nullable - List getWeightedCluster() { - return weightedClusters; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RouteAction that = (RouteAction) o; - return Objects.equals(timeoutNano, that.timeoutNano) - && Objects.equals(cluster, that.cluster) - && Objects.equals(weightedClusters, that.weightedClusters); - } - - @Override - public int hashCode() { - return Objects.hash(timeoutNano, cluster, weightedClusters); - } - - @Override - public String toString() { - ToStringHelper toStringHelper = MoreObjects.toStringHelper(this); - if (timeoutNano != null) { - toStringHelper.add("timeout", timeoutNano + "ns"); - } - if (cluster != null) { - toStringHelper.add("cluster", cluster); - } - if (weightedClusters != null) { - toStringHelper.add("weightedClusters", weightedClusters); - } - return toStringHelper.toString(); - } - - @Nullable - @VisibleForTesting - static StructOrError fromEnvoyProtoRouteAction( - io.envoyproxy.envoy.config.route.v3.RouteAction proto) { - String cluster = null; - List weightedClusters = null; - switch (proto.getClusterSpecifierCase()) { - case CLUSTER: - cluster = proto.getCluster(); - break; - case CLUSTER_HEADER: - return null; - case WEIGHTED_CLUSTERS: - List clusterWeights - = proto.getWeightedClusters().getClustersList(); - if (clusterWeights.isEmpty()) { - return StructOrError.fromError("No cluster found in weighted cluster list"); - } - weightedClusters = new ArrayList<>(); - for (io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight clusterWeight - : clusterWeights) { - StructOrError clusterWeightOrError = - ClusterWeight.fromEnvoyProtoClusterWeight(clusterWeight); - if (clusterWeightOrError.getErrorDetail() != null) { - return StructOrError.fromError("RouteAction contains invalid ClusterWeight: " - + clusterWeightOrError.getErrorDetail()); - } - weightedClusters.add(clusterWeightOrError.getStruct()); - } - // TODO(chengyuanzhang): validate if the sum of weights equals to total weight. - break; - case CLUSTERSPECIFIER_NOT_SET: - default: - return StructOrError.fromError( - "Unknown cluster specifier: " + proto.getClusterSpecifierCase()); - } - Long timeoutNano = null; - if (proto.hasMaxStreamDuration()) { - io.envoyproxy.envoy.config.route.v3.RouteAction.MaxStreamDuration maxStreamDuration - = proto.getMaxStreamDuration(); - if (maxStreamDuration.hasGrpcTimeoutHeaderMax()) { - timeoutNano = Durations.toNanos(maxStreamDuration.getGrpcTimeoutHeaderMax()); - } else if (maxStreamDuration.hasMaxStreamDuration()) { - timeoutNano = Durations.toNanos(maxStreamDuration.getMaxStreamDuration()); - } - } - return StructOrError.fromStruct(new RouteAction(timeoutNano, cluster, weightedClusters)); - } - } - - /** - * See corresponding Envoy proto message {@link - * io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight}. - */ - static final class ClusterWeight { - private final String name; - private final int weight; - @Nullable - private final HttpFault httpFault; - - @VisibleForTesting - ClusterWeight(String name, int weight, @Nullable HttpFault httpFault) { - this.name = name; - this.weight = weight; - this.httpFault = httpFault; - } - - String getName() { - return name; - } - - int getWeight() { - return weight; - } - - @Nullable - HttpFault getHttpFault() { - return httpFault; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ClusterWeight that = (ClusterWeight) o; - return weight == that.weight && Objects.equals(name, that.name) - && Objects.equals(httpFault, that.httpFault); - } - - @Override - public int hashCode() { - return Objects.hash(name, weight, httpFault); - } - - @Override - public String toString() { - ToStringHelper toStringHelper = MoreObjects.toStringHelper(this) - .add("name", name) - .add("weight", weight); - if (httpFault != null) { - toStringHelper.add("httpFault", httpFault); - } - return toStringHelper.toString(); - } - - @VisibleForTesting - static StructOrError fromEnvoyProtoClusterWeight( - io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight proto) { - HttpFault httpFault = null; - Map filterConfigMap = proto.getTypedPerFilterConfigMap(); - if (filterConfigMap.containsKey(HTTP_FAULT_FILTER_NAME)) { - Any rawFaultFilterConfig = filterConfigMap.get(HTTP_FAULT_FILTER_NAME); - StructOrError httpFaultOrError = - HttpFault.decodeFaultFilterConfig(rawFaultFilterConfig); - if (httpFaultOrError.getErrorDetail() != null) { - return StructOrError.fromError( - "ClusterWeight [" + proto.getName() + "] contains invalid HttpFault filter: " - + httpFaultOrError.getErrorDetail()); - } - httpFault = httpFaultOrError.getStruct(); - } - return StructOrError.fromStruct( - new ClusterWeight(proto.getName(), proto.getWeight().getValue(), httpFault)); - } - } - - /** - * See corresponding Envoy proto message {@link - * io.envoyproxy.envoy.config.endpoint.v3.ClusterStats}. - */ - static final class ClusterStats { - private final String clusterName; - @Nullable - private final String clusterServiceName; - private final List upstreamLocalityStatsList; - private final List droppedRequestsList; - private final long totalDroppedRequests; - private final long loadReportIntervalNanos; - - private ClusterStats( - String clusterName, - @Nullable String clusterServiceName, - List upstreamLocalityStatsList, - List droppedRequestsList, - long totalDroppedRequests, - long loadReportIntervalNanos) { - this.clusterName = checkNotNull(clusterName, "clusterName"); - this.clusterServiceName = clusterServiceName; - this.upstreamLocalityStatsList = Collections.unmodifiableList( - checkNotNull(upstreamLocalityStatsList, "upstreamLocalityStatsList")); - this.droppedRequestsList = Collections.unmodifiableList( - checkNotNull(droppedRequestsList, "dropRequestsList")); - this.totalDroppedRequests = totalDroppedRequests; - this.loadReportIntervalNanos = loadReportIntervalNanos; - } - - String getClusterName() { - return clusterName; - } - - @Nullable - String getClusterServiceName() { - return clusterServiceName; - } - - List getUpstreamLocalityStatsList() { - return upstreamLocalityStatsList; - } - - List getDroppedRequestsList() { - return droppedRequestsList; - } - - long getTotalDroppedRequests() { - return totalDroppedRequests; - } - - long getLoadReportIntervalNanos() { - return loadReportIntervalNanos; - } - - io.envoyproxy.envoy.config.endpoint.v3.ClusterStats toEnvoyProtoClusterStats() { - io.envoyproxy.envoy.config.endpoint.v3.ClusterStats.Builder builder = - io.envoyproxy.envoy.config.endpoint.v3.ClusterStats.newBuilder() - .setClusterName(clusterName); - if (clusterServiceName != null) { - builder.setClusterServiceName(clusterServiceName); - } - for (UpstreamLocalityStats upstreamLocalityStats : upstreamLocalityStatsList) { - builder.addUpstreamLocalityStats(upstreamLocalityStats.toEnvoyProtoUpstreamLocalityStats()); - } - for (DroppedRequests droppedRequests : droppedRequestsList) { - builder.addDroppedRequests(droppedRequests.toEnvoyProtoDroppedRequests()); - } - return builder - .setTotalDroppedRequests(totalDroppedRequests) - .setLoadReportInterval(Durations.fromNanos(loadReportIntervalNanos)) - .build(); - } - - io.envoyproxy.envoy.api.v2.endpoint.ClusterStats toEnvoyProtoClusterStatsV2() { - io.envoyproxy.envoy.api.v2.endpoint.ClusterStats.Builder builder = - io.envoyproxy.envoy.api.v2.endpoint.ClusterStats.newBuilder() - .setClusterName(clusterName); - if (clusterServiceName != null) { - builder.setClusterServiceName(clusterServiceName); - } - for (UpstreamLocalityStats upstreamLocalityStats : upstreamLocalityStatsList) { - builder.addUpstreamLocalityStats( - upstreamLocalityStats.toEnvoyProtoUpstreamLocalityStatsV2()); - } - for (DroppedRequests droppedRequests : droppedRequestsList) { - builder.addDroppedRequests(droppedRequests.toEnvoyProtoDroppedRequestsV2()); - } - return builder - .setTotalDroppedRequests(totalDroppedRequests) - .setLoadReportInterval(Durations.fromNanos(loadReportIntervalNanos)) - .build(); - } - - @VisibleForTesting - Builder toBuilder() { - Builder builder = new Builder() - .setClusterName(clusterName) - .setTotalDroppedRequests(totalDroppedRequests) - .setLoadReportIntervalNanos(loadReportIntervalNanos); - if (clusterServiceName != null) { - builder.setClusterServiceName(clusterServiceName); - } - for (UpstreamLocalityStats upstreamLocalityStats : upstreamLocalityStatsList) { - builder.addUpstreamLocalityStats(upstreamLocalityStats); - } - for (DroppedRequests droppedRequests : droppedRequestsList) { - builder.addDroppedRequests(droppedRequests); - } - return builder; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ClusterStats that = (ClusterStats) o; - return totalDroppedRequests == that.totalDroppedRequests - && loadReportIntervalNanos == that.loadReportIntervalNanos - && Objects.equals(clusterName, that.clusterName) - && Objects.equals(clusterServiceName, that.clusterServiceName) - && Objects.equals(upstreamLocalityStatsList, that.upstreamLocalityStatsList) - && Objects.equals(droppedRequestsList, that.droppedRequestsList); - } - - @Override - public int hashCode() { - return Objects.hash( - clusterName, clusterServiceName, upstreamLocalityStatsList, droppedRequestsList, - totalDroppedRequests, loadReportIntervalNanos); - } - - static Builder newBuilder() { - return new Builder(); - } - - static final class Builder { - private String clusterName; - private String clusterServiceName; - private final List upstreamLocalityStatsList = new ArrayList<>(); - private final List droppedRequestsList = new ArrayList<>(); - private long totalDroppedRequests; - private long loadReportIntervalNanos; - - private Builder() { - } - - Builder setClusterName(String clusterName) { - this.clusterName = checkNotNull(clusterName, "clusterName"); - return this; - } - - Builder setClusterServiceName(String clusterServiceName) { - this.clusterServiceName = checkNotNull(clusterServiceName, "clusterServiceName"); - return this; - } - - Builder setTotalDroppedRequests(long totalDroppedRequests) { - this.totalDroppedRequests = totalDroppedRequests; - return this; - } - - Builder setLoadReportIntervalNanos(long loadReportIntervalNanos) { - this.loadReportIntervalNanos = loadReportIntervalNanos; - return this; - } - - long getLoadReportIntervalNanos() { - return loadReportIntervalNanos; - } - - Builder addUpstreamLocalityStats(UpstreamLocalityStats upstreamLocalityStats) { - upstreamLocalityStatsList.add(checkNotNull(upstreamLocalityStats, "upstreamLocalityStats")); - return this; - } - - Builder addAllUpstreamLocalityStats(Collection upstreamLocalityStats) { - upstreamLocalityStatsList.addAll(upstreamLocalityStats); - return this; - } - - Builder addDroppedRequests(DroppedRequests droppedRequests) { - droppedRequestsList.add(checkNotNull(droppedRequests, "dropRequests")); - return this; - } - - ClusterStats build() { - return new ClusterStats( - clusterName, clusterServiceName,upstreamLocalityStatsList, droppedRequestsList, - totalDroppedRequests, loadReportIntervalNanos); - } - } - - /** - * See corresponding Envoy proto message {@link - * io.envoyproxy.envoy.config.endpoint.v3.ClusterStats.DroppedRequests}. - */ - static final class DroppedRequests { - private final String category; - private final long droppedCount; - - DroppedRequests(String category, long droppedCount) { - this.category = checkNotNull(category, "category"); - this.droppedCount = droppedCount; - } - - String getCategory() { - return category; - } - - long getDroppedCount() { - return droppedCount; - } - - private io.envoyproxy.envoy.config.endpoint.v3.ClusterStats.DroppedRequests - toEnvoyProtoDroppedRequests() { - return io.envoyproxy.envoy.config.endpoint.v3.ClusterStats.DroppedRequests.newBuilder() - .setCategory(category) - .setDroppedCount(droppedCount) - .build(); - } - - private io.envoyproxy.envoy.api.v2.endpoint.ClusterStats.DroppedRequests - toEnvoyProtoDroppedRequestsV2() { - return io.envoyproxy.envoy.api.v2.endpoint.ClusterStats.DroppedRequests.newBuilder() - .setCategory(category) - .setDroppedCount(droppedCount) - .build(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - DroppedRequests that = (DroppedRequests) o; - return droppedCount == that.droppedCount && Objects.equals(category, that.category); - } - - @Override - public int hashCode() { - return Objects.hash(category, droppedCount); - } - } - } - - /** - * See corresponding Envoy proto message {@link - * io.envoyproxy.envoy.config.endpoint.v3.UpstreamLocalityStats}. - */ - static final class UpstreamLocalityStats { - private final Locality locality; - private final long totalSuccessfulRequests; - private final long totalErrorRequests; - private final long totalRequestsInProgress; - private final long totalIssuedRequests; - private final List loadMetricStatsList; - - private UpstreamLocalityStats( - Locality locality, - long totalSuccessfulRequests, - long totalErrorRequests, - long totalRequestsInProgress, - long totalIssuedRequests, - List loadMetricStatsList) { - this.locality = checkNotNull(locality, "locality"); - this.totalSuccessfulRequests = totalSuccessfulRequests; - this.totalErrorRequests = totalErrorRequests; - this.totalRequestsInProgress = totalRequestsInProgress; - this.totalIssuedRequests = totalIssuedRequests; - this.loadMetricStatsList = Collections.unmodifiableList( - checkNotNull(loadMetricStatsList, "loadMetricStatsList")); - } - - Locality getLocality() { - return locality; - } - - long getTotalSuccessfulRequests() { - return totalSuccessfulRequests; - } - - long getTotalErrorRequests() { - return totalErrorRequests; - } - - long getTotalRequestsInProgress() { - return totalRequestsInProgress; - } - - long getTotalIssuedRequests() { - return totalIssuedRequests; - } - - List getLoadMetricStatsList() { - return loadMetricStatsList; - } - - private io.envoyproxy.envoy.config.endpoint.v3.UpstreamLocalityStats - toEnvoyProtoUpstreamLocalityStats() { - io.envoyproxy.envoy.config.endpoint.v3.UpstreamLocalityStats.Builder builder - = io.envoyproxy.envoy.config.endpoint.v3.UpstreamLocalityStats.newBuilder() - .setLocality(locality.toEnvoyProtoLocality()) - .setTotalSuccessfulRequests(totalSuccessfulRequests) - .setTotalErrorRequests(totalErrorRequests) - .setTotalRequestsInProgress(totalRequestsInProgress) - .setTotalIssuedRequests(totalIssuedRequests); - for (EndpointLoadMetricStats endpointLoadMetricStats : loadMetricStatsList) { - builder.addLoadMetricStats(endpointLoadMetricStats.toEnvoyProtoEndpointLoadMetricStats()); - } - return builder.build(); - } - - private io.envoyproxy.envoy.api.v2.endpoint.UpstreamLocalityStats - toEnvoyProtoUpstreamLocalityStatsV2() { - io.envoyproxy.envoy.api.v2.endpoint.UpstreamLocalityStats.Builder builder - = io.envoyproxy.envoy.api.v2.endpoint.UpstreamLocalityStats.newBuilder() - .setLocality(locality.toEnvoyProtoLocalityV2()) - .setTotalSuccessfulRequests(totalSuccessfulRequests) - .setTotalErrorRequests(totalErrorRequests) - .setTotalRequestsInProgress(totalRequestsInProgress) - .setTotalIssuedRequests(totalIssuedRequests); - for (EndpointLoadMetricStats endpointLoadMetricStats : loadMetricStatsList) { - builder.addLoadMetricStats(endpointLoadMetricStats.toEnvoyProtoEndpointLoadMetricStatsV2()); - } - return builder.build(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - UpstreamLocalityStats that = (UpstreamLocalityStats) o; - return totalSuccessfulRequests == that.totalSuccessfulRequests - && totalErrorRequests == that.totalErrorRequests - && totalRequestsInProgress == that.totalRequestsInProgress - && totalIssuedRequests == that.totalIssuedRequests - && Objects.equals(locality, that.locality) - && Objects.equals(loadMetricStatsList, that.loadMetricStatsList); - } - - @Override - public int hashCode() { - return Objects.hash( - locality, totalSuccessfulRequests, totalErrorRequests, totalRequestsInProgress, - totalIssuedRequests, loadMetricStatsList); - } - - static Builder newBuilder() { - return new Builder(); - } - - static final class Builder { - private Locality locality; - private long totalSuccessfulRequests; - private long totalErrorRequests; - private long totalRequestsInProgress; - private long totalIssuedRequests; - private final List loadMetricStatsList = new ArrayList<>(); - - private Builder() { - } - - Builder setLocality(Locality locality) { - this.locality = checkNotNull(locality, "locality"); - return this; - } - - Builder setTotalSuccessfulRequests(long totalSuccessfulRequests) { - this.totalSuccessfulRequests = totalSuccessfulRequests; - return this; - } - - Builder setTotalErrorRequests(long totalErrorRequests) { - this.totalErrorRequests = totalErrorRequests; - return this; - } - - Builder setTotalRequestsInProgress(long totalRequestsInProgress) { - this.totalRequestsInProgress = totalRequestsInProgress; - return this; - } - - Builder setTotalIssuedRequests(long totalIssuedRequests) { - this.totalIssuedRequests = totalIssuedRequests; - return this; - } - - Builder addLoadMetricStats(EndpointLoadMetricStats endpointLoadMetricStats) { - loadMetricStatsList.add(checkNotNull(endpointLoadMetricStats, "endpointLoadMetricStats")); - return this; - } - - Builder addAllLoadMetricStats(Collection endpointLoadMetricStats) { - loadMetricStatsList.addAll( - checkNotNull(endpointLoadMetricStats, "endpointLoadMetricStats")); - return this; - } - - UpstreamLocalityStats build() { - return new UpstreamLocalityStats( - locality, totalSuccessfulRequests, totalErrorRequests, totalRequestsInProgress, - totalIssuedRequests, loadMetricStatsList); - } - } - } - - /** - * See corresponding Envoy proto message {@link - * io.envoyproxy.envoy.config.endpoint.v3.EndpointLoadMetricStats}. - */ - static final class EndpointLoadMetricStats { - private final String metricName; - private final long numRequestsFinishedWithMetric; - private final double totalMetricValue; - - private EndpointLoadMetricStats(String metricName, long numRequestsFinishedWithMetric, - double totalMetricValue) { - this.metricName = checkNotNull(metricName, "metricName"); - this.numRequestsFinishedWithMetric = numRequestsFinishedWithMetric; - this.totalMetricValue = totalMetricValue; - } - - String getMetricName() { - return metricName; - } - - long getNumRequestsFinishedWithMetric() { - return numRequestsFinishedWithMetric; - } - - double getTotalMetricValue() { - return totalMetricValue; - } - - private io.envoyproxy.envoy.config.endpoint.v3.EndpointLoadMetricStats - toEnvoyProtoEndpointLoadMetricStats() { - return io.envoyproxy.envoy.config.endpoint.v3.EndpointLoadMetricStats.newBuilder() - .setMetricName(metricName) - .setNumRequestsFinishedWithMetric(numRequestsFinishedWithMetric) - .setTotalMetricValue(totalMetricValue) - .build(); - } - - private io.envoyproxy.envoy.api.v2.endpoint.EndpointLoadMetricStats - toEnvoyProtoEndpointLoadMetricStatsV2() { - return io.envoyproxy.envoy.api.v2.endpoint.EndpointLoadMetricStats.newBuilder() - .setMetricName(metricName) - .setNumRequestsFinishedWithMetric(numRequestsFinishedWithMetric) - .setTotalMetricValue(totalMetricValue) - .build(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - EndpointLoadMetricStats that = (EndpointLoadMetricStats) o; - return numRequestsFinishedWithMetric == that.numRequestsFinishedWithMetric - && Double.compare(that.totalMetricValue, totalMetricValue) == 0 - && Objects.equals(metricName, that.metricName); - } - - @Override - public int hashCode() { - return Objects.hash(metricName, numRequestsFinishedWithMetric, totalMetricValue); - } - - static Builder newBuilder() { - return new Builder(); - } - - static final class Builder { - private String metricName; - private long numRequestsFinishedWithMetric; - private double totalMetricValue; - - private Builder() { - } - - Builder setMetricName(String metricName) { - this.metricName = checkNotNull(metricName, "metricName"); - return this; - } - - Builder setNumRequestsFinishedWithMetric(long numRequestsFinishedWithMetric) { - this.numRequestsFinishedWithMetric = numRequestsFinishedWithMetric; - return this; - } - - Builder setTotalMetricValue(double totalMetricValue) { - this.totalMetricValue = totalMetricValue; - return this; - } - - EndpointLoadMetricStats build() { - return new EndpointLoadMetricStats( - metricName, numRequestsFinishedWithMetric, totalMetricValue); - } - } - } } diff --git a/xds/src/main/java/io/grpc/xds/HttpFault.java b/xds/src/main/java/io/grpc/xds/HttpFault.java new file mode 100644 index 0000000000..c6dd4f7a66 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/HttpFault.java @@ -0,0 +1,101 @@ +/* + * Copyright 2021 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.base.Preconditions.checkNotNull; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import io.grpc.Status; +import io.grpc.xds.Matchers.HeaderMatcher; +import java.util.List; +import javax.annotation.Nullable; + +/** Fault injection configurations. */ +@AutoValue +abstract class HttpFault { + @Nullable + abstract FaultDelay faultDelay(); + + @Nullable + abstract FaultAbort faultAbort(); + + abstract String upstreamCluster(); + + abstract ImmutableList downstreamNodes(); + + abstract ImmutableList headers(); + + @Nullable + abstract Integer maxActiveFaults(); + + static HttpFault create(@Nullable FaultDelay faultDelay, @Nullable FaultAbort faultAbort, + String upstreamCluster, List downstreamNodes, List headers, + @Nullable Integer maxActiveFaults) { + return new AutoValue_HttpFault(faultDelay, faultAbort, upstreamCluster, + ImmutableList.copyOf(downstreamNodes), ImmutableList.copyOf(headers), maxActiveFaults); + } + + /** Fault configurations for aborting requests. */ + @AutoValue + abstract static class FaultDelay { + @Nullable + abstract Long delayNanos(); + + abstract boolean headerDelay(); + + abstract int ratePerMillion(); + + static FaultDelay forFixedDelay(long delayNanos, int ratePerMillion) { + return FaultDelay.create(delayNanos, false, ratePerMillion); + } + + static FaultDelay forHeader(int ratePerMillion) { + return FaultDelay.create(null, true, ratePerMillion); + } + + private static FaultDelay create( + @Nullable Long delayNanos, boolean headerDelay, int ratePerMillion) { + return new AutoValue_HttpFault_FaultDelay(delayNanos, headerDelay, ratePerMillion); + } + } + + /** Fault configurations for delaying requests. */ + @AutoValue + abstract static class FaultAbort { + @Nullable + abstract Status status(); + + abstract boolean headerAbort(); + + abstract int ratePerMillion(); + + static FaultAbort forStatus(Status status, int ratePerMillion) { + checkNotNull(status, "status"); + return FaultAbort.create(status, false, ratePerMillion); + } + + static FaultAbort forHeader(int ratePerMillion) { + return FaultAbort.create(null, true, ratePerMillion); + } + + private static FaultAbort create( + @Nullable Status status, boolean headerAbort, int ratePerMillion) { + return new AutoValue_HttpFault_FaultAbort(status, headerAbort, ratePerMillion); + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/InternalXdsAttributes.java b/xds/src/main/java/io/grpc/xds/InternalXdsAttributes.java index 7bc733663d..6f3062f22b 100644 --- a/xds/src/main/java/io/grpc/xds/InternalXdsAttributes.java +++ b/xds/src/main/java/io/grpc/xds/InternalXdsAttributes.java @@ -22,7 +22,6 @@ import io.grpc.Grpc; import io.grpc.Internal; import io.grpc.NameResolver; import io.grpc.internal.ObjectPool; -import io.grpc.xds.EnvoyProtoData.Locality; import io.grpc.xds.XdsNameResolverProvider.CallCounterProvider; import io.grpc.xds.internal.sds.SslContextProviderSupplier; diff --git a/xds/src/main/java/io/grpc/xds/LoadReportClient.java b/xds/src/main/java/io/grpc/xds/LoadReportClient.java index f6f931e01c..603e3dcd6a 100644 --- a/xds/src/main/java/io/grpc/xds/LoadReportClient.java +++ b/xds/src/main/java/io/grpc/xds/LoadReportClient.java @@ -35,8 +35,10 @@ import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext.ScheduledHandle; import io.grpc.internal.BackoffPolicy; import io.grpc.stub.StreamObserver; -import io.grpc.xds.EnvoyProtoData.ClusterStats; import io.grpc.xds.EnvoyProtoData.Node; +import io.grpc.xds.Stats.ClusterStats; +import io.grpc.xds.Stats.DroppedRequests; +import io.grpc.xds.Stats.UpstreamLocalityStats; import io.grpc.xds.XdsLogger.XdsLogLevel; import java.util.ArrayList; import java.util.Collections; @@ -346,7 +348,7 @@ final class LoadReportClient { io.envoyproxy.envoy.service.load_stats.v2.LoadStatsRequest.newBuilder() .setNode(node.toEnvoyProtoNodeV2()); for (ClusterStats stats : clusterStatsList) { - requestBuilder.addClusterStats(stats.toEnvoyProtoClusterStatsV2()); + requestBuilder.addClusterStats(buildClusterStats(stats)); } io.envoyproxy.envoy.service.load_stats.v2.LoadStatsRequest request = requestBuilder.build(); lrsRequestWriterV2.onNext(requestBuilder.build()); @@ -357,6 +359,37 @@ final class LoadReportClient { void sendError(Exception error) { lrsRequestWriterV2.onError(error); } + + private io.envoyproxy.envoy.api.v2.endpoint.ClusterStats buildClusterStats( + ClusterStats stats) { + io.envoyproxy.envoy.api.v2.endpoint.ClusterStats.Builder builder = + io.envoyproxy.envoy.api.v2.endpoint.ClusterStats.newBuilder() + .setClusterName(stats.clusterName()); + if (stats.clusterServiceName() != null) { + builder.setClusterServiceName(stats.clusterServiceName()); + } + for (UpstreamLocalityStats upstreamLocalityStats : stats.upstreamLocalityStatsList()) { + builder.addUpstreamLocalityStats( + io.envoyproxy.envoy.api.v2.endpoint.UpstreamLocalityStats.newBuilder() + .setLocality( + io.envoyproxy.envoy.api.v2.core.Locality.newBuilder() + .setRegion(upstreamLocalityStats.locality().region()) + .setZone(upstreamLocalityStats.locality().zone()) + .setSubZone(upstreamLocalityStats.locality().subZone())) + .setTotalSuccessfulRequests(upstreamLocalityStats.totalSuccessfulRequests()) + .setTotalErrorRequests(upstreamLocalityStats.totalErrorRequests()) + .setTotalRequestsInProgress(upstreamLocalityStats.totalRequestsInProgress()) + .setTotalIssuedRequests(upstreamLocalityStats.totalIssuedRequests())); + } + for (DroppedRequests droppedRequests : stats.droppedRequestsList()) { + builder.addDroppedRequests( + io.envoyproxy.envoy.api.v2.endpoint.ClusterStats.DroppedRequests.newBuilder() + .setCategory(droppedRequests.category()) + .setDroppedCount(droppedRequests.droppedCount())); + } + return builder.setTotalDroppedRequests(stats.totalDroppedRequests()) + .setLoadReportInterval(Durations.fromNanos(stats.loadReportIntervalNano())).build(); + } } private final class LrsStreamV3 extends LrsStream { @@ -410,7 +443,7 @@ final class LoadReportClient { LoadStatsRequest.Builder requestBuilder = LoadStatsRequest.newBuilder().setNode(node.toEnvoyProtoNode()); for (ClusterStats stats : clusterStatsList) { - requestBuilder.addClusterStats(stats.toEnvoyProtoClusterStats()); + requestBuilder.addClusterStats(buildClusterStats(stats)); } LoadStatsRequest request = requestBuilder.build(); lrsRequestWriterV3.onNext(request); @@ -421,5 +454,38 @@ final class LoadReportClient { void sendError(Exception error) { lrsRequestWriterV3.onError(error); } + + private io.envoyproxy.envoy.config.endpoint.v3.ClusterStats buildClusterStats( + ClusterStats stats) { + io.envoyproxy.envoy.config.endpoint.v3.ClusterStats.Builder builder = + io.envoyproxy.envoy.config.endpoint.v3.ClusterStats.newBuilder() + .setClusterName(stats.clusterName()); + if (stats.clusterServiceName() != null) { + builder.setClusterServiceName(stats.clusterServiceName()); + } + for (UpstreamLocalityStats upstreamLocalityStats : stats.upstreamLocalityStatsList()) { + builder.addUpstreamLocalityStats( + io.envoyproxy.envoy.config.endpoint.v3.UpstreamLocalityStats.newBuilder() + .setLocality( + io.envoyproxy.envoy.config.core.v3.Locality.newBuilder() + .setRegion(upstreamLocalityStats.locality().region()) + .setZone(upstreamLocalityStats.locality().zone()) + .setSubZone(upstreamLocalityStats.locality().subZone())) + .setTotalSuccessfulRequests(upstreamLocalityStats.totalSuccessfulRequests()) + .setTotalErrorRequests(upstreamLocalityStats.totalErrorRequests()) + .setTotalRequestsInProgress(upstreamLocalityStats.totalRequestsInProgress()) + .setTotalIssuedRequests(upstreamLocalityStats.totalIssuedRequests())); + } + for (DroppedRequests droppedRequests : stats.droppedRequestsList()) { + builder.addDroppedRequests( + io.envoyproxy.envoy.config.endpoint.v3.ClusterStats.DroppedRequests.newBuilder() + .setCategory(droppedRequests.category()) + .setDroppedCount(droppedRequests.droppedCount())); + } + return builder + .setTotalDroppedRequests(stats.totalDroppedRequests()) + .setLoadReportInterval(Durations.fromNanos(stats.loadReportIntervalNano())) + .build(); + } } } diff --git a/xds/src/main/java/io/grpc/xds/LoadStatsManager2.java b/xds/src/main/java/io/grpc/xds/LoadStatsManager2.java index 91c6bab58b..4bd0ba437b 100644 --- a/xds/src/main/java/io/grpc/xds/LoadStatsManager2.java +++ b/xds/src/main/java/io/grpc/xds/LoadStatsManager2.java @@ -23,10 +23,9 @@ import com.google.common.base.Stopwatch; import com.google.common.base.Supplier; import com.google.common.collect.Sets; import io.grpc.Status; -import io.grpc.xds.EnvoyProtoData.ClusterStats; -import io.grpc.xds.EnvoyProtoData.ClusterStats.DroppedRequests; -import io.grpc.xds.EnvoyProtoData.Locality; -import io.grpc.xds.EnvoyProtoData.UpstreamLocalityStats; +import io.grpc.xds.Stats.ClusterStats; +import io.grpc.xds.Stats.DroppedRequests; +import io.grpc.xds.Stats.UpstreamLocalityStats; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -153,9 +152,9 @@ final class LoadStatsManager2 { if (clusterDropStats != null) { Set toDiscard = new HashSet<>(); for (String edsServiceName : clusterDropStats.keySet()) { - ClusterStats.Builder builder = ClusterStats.newBuilder().setClusterName(cluster); + ClusterStats.Builder builder = ClusterStats.newBuilder().clusterName(cluster); if (edsServiceName != null) { - builder.setClusterServiceName(edsServiceName); + builder.clusterServiceName(edsServiceName); } ReferenceCounted ref = clusterDropStats.get(edsServiceName); if (ref.getReferenceCount() == 0) { // stats object no longer needed after snapshot @@ -164,12 +163,12 @@ final class LoadStatsManager2 { ClusterDropStatsSnapshot dropStatsSnapshot = ref.get().snapshot(); long totalCategorizedDrops = 0L; for (Map.Entry entry : dropStatsSnapshot.categorizedDrops.entrySet()) { - builder.addDroppedRequests(new DroppedRequests(entry.getKey(), entry.getValue())); + builder.addDroppedRequests(DroppedRequests.create(entry.getKey(), entry.getValue())); totalCategorizedDrops += entry.getValue(); } - builder.setTotalDroppedRequests( + builder.totalDroppedRequests( totalCategorizedDrops + dropStatsSnapshot.uncategorizedDrops); - builder.setLoadReportIntervalNanos(dropStatsSnapshot.durationNano); + builder.loadReportIntervalNano(dropStatsSnapshot.durationNano); statsReportBuilders.put(edsServiceName, builder); } clusterDropStats.keySet().removeAll(toDiscard); @@ -180,9 +179,9 @@ final class LoadStatsManager2 { for (String edsServiceName : clusterLoadStats.keySet()) { ClusterStats.Builder builder = statsReportBuilders.get(edsServiceName); if (builder == null) { - builder = ClusterStats.newBuilder().setClusterName(cluster); + builder = ClusterStats.newBuilder().clusterName(cluster); if (edsServiceName != null) { - builder.setClusterServiceName(edsServiceName); + builder.clusterServiceName(edsServiceName); } statsReportBuilders.put(edsServiceName, builder); } @@ -196,17 +195,14 @@ final class LoadStatsManager2 { if (ref.getReferenceCount() == 0 && snapshot.callsInProgress == 0) { localitiesToDiscard.add(locality); } - UpstreamLocalityStats.Builder localityStatsBuilder = UpstreamLocalityStats.newBuilder(); - localityStatsBuilder.setLocality(locality); - localityStatsBuilder.setTotalIssuedRequests(snapshot.callsIssued); - localityStatsBuilder.setTotalSuccessfulRequests(snapshot.callsSucceeded); - localityStatsBuilder.setTotalErrorRequests(snapshot.callsFailed); - localityStatsBuilder.setTotalRequestsInProgress(snapshot.callsInProgress); - builder.addUpstreamLocalityStats(localityStatsBuilder.build()); + UpstreamLocalityStats upstreamLocalityStats = UpstreamLocalityStats.create( + locality, snapshot.callsIssued, snapshot.callsSucceeded, snapshot.callsFailed, + snapshot.callsInProgress); + builder.addUpstreamLocalityStats(upstreamLocalityStats); // Use the max (drops/loads) recording interval as the overall interval for the // cluster's stats. In general, they should be mostly identical. - builder.setLoadReportIntervalNanos( - Math.max(builder.getLoadReportIntervalNanos(), snapshot.durationNano)); + builder.loadReportIntervalNano( + Math.max(builder.loadReportIntervalNano(), snapshot.durationNano)); } localityStats.keySet().removeAll(localitiesToDiscard); if (localityStats.isEmpty()) { diff --git a/xds/src/main/java/io/grpc/xds/Locality.java b/xds/src/main/java/io/grpc/xds/Locality.java new file mode 100644 index 0000000000..caa8a01a28 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/Locality.java @@ -0,0 +1,33 @@ +/* + * Copyright 2021 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.auto.value.AutoValue; + +/** Represents a network locality. */ +@AutoValue +abstract class Locality { + abstract String region(); + + abstract String zone(); + + abstract String subZone(); + + static Locality create(String region, String zone, String subZone) { + return new AutoValue_Locality(region, zone, subZone); + } +} diff --git a/xds/src/main/java/io/grpc/xds/Matchers.java b/xds/src/main/java/io/grpc/xds/Matchers.java new file mode 100644 index 0000000000..8018dc5ffa --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/Matchers.java @@ -0,0 +1,171 @@ +/* + * Copyright 2021 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.base.Preconditions.checkNotNull; + +import com.google.auto.value.AutoValue; +import com.google.re2j.Pattern; +import javax.annotation.Nullable; + +/** A group of request matchers. */ +final class Matchers { + private Matchers() {} + + /** Matcher for HTTP request path. */ + @AutoValue + abstract static class PathMatcher { + // Exact full path to be matched. + @Nullable + abstract String path(); + + // Path prefix to be matched. + @Nullable + abstract String prefix(); + + // Regular expression pattern of the path to be matched. + @Nullable + abstract Pattern regEx(); + + // Whether case sensitivity is taken into account for matching. + // Only valid for full path matching or prefix matching. + abstract boolean caseSensitive(); + + static PathMatcher fromPath(String path, boolean caseSensitive) { + checkNotNull(path, "path"); + return PathMatcher.create(path, null, null, caseSensitive); + } + + static PathMatcher fromPrefix(String prefix, boolean caseSensitive) { + checkNotNull(prefix, "prefix"); + return PathMatcher.create(null, prefix, null, caseSensitive); + } + + static PathMatcher fromRegEx(Pattern regEx) { + checkNotNull(regEx, "regEx"); + return PathMatcher.create(null, null, regEx, false /* doesn't matter */); + } + + private static PathMatcher create(@Nullable String path, @Nullable String prefix, + @Nullable Pattern regEx, boolean caseSensitive) { + return new AutoValue_Matchers_PathMatcher(path, prefix, regEx, caseSensitive); + } + } + + /** Matcher for HTTP request headers. */ + @AutoValue + abstract static class HeaderMatcher { + // Name of the header to be matched. + abstract String name(); + + // Matches exact header value. + @Nullable + abstract String exactValue(); + + // Matches header value with the regular expression pattern. + @Nullable + abstract Pattern safeRegEx(); + + // Matches header value an integer value in the range. + @Nullable + abstract Range range(); + + // Matches header presence. + @Nullable + abstract Boolean present(); + + // Matches header value with the prefix. + @Nullable + abstract String prefix(); + + // Matches header value with the suffix. + @Nullable + abstract String suffix(); + + // Whether the matching semantics is inverted. E.g., present && !inverted -> !present + abstract boolean inverted(); + + static HeaderMatcher forExactValue(String name, String exactValue, boolean inverted) { + checkNotNull(name, "name"); + checkNotNull(exactValue, "exactValue"); + return HeaderMatcher.create(name, exactValue, null, null, null, null, null, inverted); + } + + static HeaderMatcher forSafeRegEx(String name, Pattern safeRegEx, boolean inverted) { + checkNotNull(name, "name"); + checkNotNull(safeRegEx, "safeRegEx"); + return HeaderMatcher.create(name, null, safeRegEx, null, null, null, null, inverted); + } + + static HeaderMatcher forRange(String name, Range range, boolean inverted) { + checkNotNull(name, "name"); + checkNotNull(range, "range"); + return HeaderMatcher.create(name, null, null, range, null, null, null, inverted); + } + + static HeaderMatcher forPresent(String name, boolean present, boolean inverted) { + checkNotNull(name, "name"); + return HeaderMatcher.create(name, null, null, null, present, null, null, inverted); + } + + static HeaderMatcher forPrefix(String name, String prefix, boolean inverted) { + checkNotNull(name, "name"); + checkNotNull(prefix, "prefix"); + return HeaderMatcher.create(name, null, null, null, null, prefix, null, inverted); + } + + static HeaderMatcher forSuffix(String name, String suffix, boolean inverted) { + checkNotNull(name, "name"); + checkNotNull(suffix, "suffix"); + return HeaderMatcher.create(name, null, null, null, null, null, suffix, inverted); + } + + private static HeaderMatcher create(String name, @Nullable String exactValue, + @Nullable Pattern safeRegEx, @Nullable Range range, + @Nullable Boolean present, @Nullable String prefix, + @Nullable String suffix, boolean inverted) { + checkNotNull(name, "name"); + return new AutoValue_Matchers_HeaderMatcher(name, exactValue, safeRegEx, range, present, + prefix, suffix, inverted); + } + + /** Represents an integer range. */ + @AutoValue + abstract static class Range { + abstract long start(); + + abstract long end(); + + static Range create(long start, long end) { + return new AutoValue_Matchers_HeaderMatcher_Range(start, end); + } + + } + } + + /** Represents a fractional value. */ + @AutoValue + abstract static class FractionMatcher { + abstract int numerator(); + + abstract int denominator(); + + static FractionMatcher create(int numerator, int denominator) { + return new AutoValue_Matchers_FractionMatcher(numerator, denominator); + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/RouteMatch.java b/xds/src/main/java/io/grpc/xds/RouteMatch.java deleted file mode 100644 index 127ffb4a7c..0000000000 --- a/xds/src/main/java/io/grpc/xds/RouteMatch.java +++ /dev/null @@ -1,447 +0,0 @@ -/* - * 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.Joiner; -import com.google.common.base.MoreObjects; -import com.google.common.base.MoreObjects.ToStringHelper; -import com.google.re2j.Pattern; -import io.grpc.xds.ThreadSafeRandom.ThreadSafeRandomImpl; -import java.util.Collections; -import java.util.List; -import java.util.Map; -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; - } - - static RouteMatch withPathExactOnly(String pathExact) { - return new RouteMatch(PathMatcher.fromPath(pathExact, true), - Collections.emptyList(), null); - } - - /** - * Returns {@code true} if a request with the given path and headers passes all the rules - * specified by this RouteMatch. - * - *

The request's headers are given as a key-values mapping, where multiple values can - * be mapped to the same key. - * - *

Match is not deterministic if a runtime fraction match rule presents in this RouteMatch. - */ - boolean matches(String path, Map> headers) { - if (!pathMatch.matches(path)) { - return false; - } - for (HeaderMatcher headerMatcher : headerMatchers) { - Iterable headerValues = headers.get(headerMatcher.getName()); - // Special cases for hiding headers: "grpc-previous-rpc-attempts". - if (headerMatcher.getName().equals("grpc-previous-rpc-attempts")) { - headerValues = null; - } - // Special case for exposing headers: "content-type". - if (headerMatcher.getName().equals("content-type")) { - headerValues = Collections.singletonList("application/grpc"); - } - if (!headerMatcher.matchesValue(headerValues)) { - return false; - } - } - return fractionMatch == null || fractionMatch.matches(); - } - - @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; - private final boolean caseSensitive; - - private PathMatcher(@Nullable String path, @Nullable String prefix, @Nullable Pattern regEx, - boolean caseSensitive) { - this.path = path; - this.prefix = prefix; - this.regEx = regEx; - this.caseSensitive = caseSensitive; - } - - static PathMatcher fromPath(String path, boolean caseSensitive) { - return new PathMatcher(path, null, null, caseSensitive); - } - - static PathMatcher fromPrefix(String prefix, boolean caseSensitive) { - return new PathMatcher(null, prefix, null, caseSensitive); - } - - static PathMatcher fromRegEx(Pattern regEx) { - return new PathMatcher(null, null, regEx, false /* doesn't matter */); - } - - boolean matches(String fullMethodName) { - if (path != null) { - return caseSensitive ? path.equals(fullMethodName) : path.equalsIgnoreCase(fullMethodName); - } else if (prefix != null) { - return caseSensitive - ? fullMethodName.startsWith(prefix) - : fullMethodName.toLowerCase().startsWith(prefix.toLowerCase()); - } - return regEx.matches(fullMethodName); - } - - @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(caseSensitive, that.caseSensitive) - && Objects.equals( - regEx == null ? null : regEx.pattern(), - that.regEx == null ? null : that.regEx.pattern()); - } - - @Override - public int hashCode() { - return Objects.hash(path, prefix, caseSensitive, regEx == null ? null : regEx.pattern()); - } - - @Override - public String toString() { - ToStringHelper toStringHelper = - MoreObjects.toStringHelper(this); - if (path != null) { - toStringHelper.add("path", path).add("caseSensitive", caseSensitive); - } - if (prefix != null) { - toStringHelper.add("prefix", prefix).add("caseSensitive", caseSensitive); - } - 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; - } - - private boolean matchesValue(@Nullable Iterable values) { - if (presentMatch != null) { - return (values == null) == presentMatch.equals(isInvertedMatch); - } - if (values == null) { - return false; - } - String valueStr = Joiner.on(",").join(values); - boolean baseMatch; - if (exactMatch != null) { - baseMatch = exactMatch.equals(valueStr); - } else if (safeRegExMatch != null) { - baseMatch = safeRegExMatch.matches(valueStr); - } else if (rangeMatch != null) { - long numValue; - try { - numValue = Long.parseLong(valueStr); - baseMatch = rangeMatch.contains(numValue); - } catch (NumberFormatException ignored) { - baseMatch = false; - } - } else if (prefixMatch != null) { - baseMatch = valueStr.startsWith(prefixMatch); - } else { - baseMatch = valueStr.endsWith(suffixMatch); - } - return baseMatch != 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; - } - - boolean contains(long value) { - return value >= start && value < 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; - private final ThreadSafeRandom rand; - - FractionMatcher(int numerator, int denominator) { - this(numerator, denominator, ThreadSafeRandomImpl.instance); - } - - @VisibleForTesting - FractionMatcher(int numerator, int denominator, ThreadSafeRandom rand) { - this.numerator = numerator; - this.denominator = denominator; - this.rand = rand; - } - - private boolean matches() { - return rand.nextInt(denominator) < numerator; - } - - 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/Stats.java b/xds/src/main/java/io/grpc/xds/Stats.java new file mode 100644 index 0000000000..7e5fa8639d --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/Stats.java @@ -0,0 +1,110 @@ +/* + * Copyright 2021 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.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import javax.annotation.Nullable; + +/** Represents client load stats. */ +final class Stats { + private Stats() {} + + /** Cluster-level load stats. */ + @AutoValue + abstract static class ClusterStats { + abstract String clusterName(); + + @Nullable + abstract String clusterServiceName(); + + abstract ImmutableList upstreamLocalityStatsList(); + + abstract ImmutableList droppedRequestsList(); + + abstract long totalDroppedRequests(); + + abstract long loadReportIntervalNano(); + + static Builder newBuilder() { + return new AutoValue_Stats_ClusterStats.Builder() + .totalDroppedRequests(0L) // default initialization + .loadReportIntervalNano(0L); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder clusterName(String clusterName); + + abstract Builder clusterServiceName(String clusterServiceName); + + abstract ImmutableList.Builder upstreamLocalityStatsListBuilder(); + + Builder addUpstreamLocalityStats(UpstreamLocalityStats upstreamLocalityStats) { + upstreamLocalityStatsListBuilder().add(upstreamLocalityStats); + return this; + } + + abstract ImmutableList.Builder droppedRequestsListBuilder(); + + Builder addDroppedRequests(DroppedRequests droppedRequests) { + droppedRequestsListBuilder().add(droppedRequests); + return this; + } + + abstract Builder totalDroppedRequests(long totalDroppedRequests); + + abstract Builder loadReportIntervalNano(long loadReportIntervalNano); + + abstract long loadReportIntervalNano(); + + abstract ClusterStats build(); + } + } + + /** Stats for dropped requests. */ + @AutoValue + abstract static class DroppedRequests { + abstract String category(); + + abstract long droppedCount(); + + static DroppedRequests create(String category, long droppedCount) { + return new AutoValue_Stats_DroppedRequests(category, droppedCount); + } + } + + /** Load stats aggregated in locality level. */ + @AutoValue + abstract static class UpstreamLocalityStats { + abstract Locality locality(); + + abstract long totalIssuedRequests(); + + abstract long totalSuccessfulRequests(); + + abstract long totalErrorRequests(); + + abstract long totalRequestsInProgress(); + + static UpstreamLocalityStats create(Locality locality, long totalIssuedRequests, + long totalSuccessfulRequests, long totalErrorRequests, long totalRequestsInProgress) { + return new AutoValue_Stats_UpstreamLocalityStats(locality, totalIssuedRequests, + totalSuccessfulRequests, totalErrorRequests, totalRequestsInProgress); + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/VirtualHost.java b/xds/src/main/java/io/grpc/xds/VirtualHost.java new file mode 100644 index 0000000000..f80106b2d3 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/VirtualHost.java @@ -0,0 +1,135 @@ +/* + * Copyright 2021 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.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import io.grpc.xds.Matchers.FractionMatcher; +import io.grpc.xds.Matchers.HeaderMatcher; +import io.grpc.xds.Matchers.PathMatcher; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +/** Reprsents an upstream virtual host. */ +@AutoValue +abstract class VirtualHost { + // The canonical name of this virtual host. + abstract String name(); + + // The list of domains (host/authority header) that will be matched to this virtual host. + abstract ImmutableList domains(); + + // The list of routes that will be matched, in order, for incoming requests. + abstract ImmutableList routes(); + + @Nullable + abstract HttpFault httpFault(); + + public static VirtualHost create(String name, List domains, List routes, + @Nullable HttpFault httpFault) { + return new AutoValue_VirtualHost(name, ImmutableList.copyOf(domains), + ImmutableList.copyOf(routes), httpFault); + } + + @AutoValue + abstract static class Route { + abstract RouteMatch routeMatch(); + + abstract RouteAction routeAction(); + + @Nullable + abstract HttpFault httpFault(); + + static Route create(RouteMatch routeMatch, RouteAction routeAction, + @Nullable HttpFault httpFault) { + return new AutoValue_VirtualHost_Route(routeMatch, routeAction, httpFault); + } + + @AutoValue + abstract static class RouteMatch { + abstract PathMatcher pathMatcher(); + + abstract ImmutableList headerMatchers(); + + @Nullable + abstract FractionMatcher fractionMatcher(); + + // TODO(chengyuanzhang): maybe delete me. + @VisibleForTesting + static RouteMatch withPathExactOnly(String path) { + return RouteMatch.create(PathMatcher.fromPath(path, true), + Collections.emptyList(), null); + } + + static RouteMatch create(PathMatcher pathMatcher, + List headerMatchers, @Nullable FractionMatcher fractionMatcher) { + return new AutoValue_VirtualHost_Route_RouteMatch(pathMatcher, + ImmutableList.copyOf(headerMatchers), fractionMatcher); + } + } + + @AutoValue + abstract static class RouteAction { + @Nullable + abstract Long timeoutNano(); + + @Nullable + abstract String cluster(); + + @Nullable + abstract ImmutableList weightedClusters(); + + static RouteAction forCluster(String cluster, @Nullable Long timeoutNano) { + checkNotNull(cluster, "cluster"); + return RouteAction.create(timeoutNano, cluster, null); + } + + static RouteAction forWeightedClusters(List weightedClusters, + @Nullable Long timeoutNano) { + checkNotNull(weightedClusters, "weightedClusters"); + checkArgument(!weightedClusters.isEmpty(), "empty cluster list"); + return RouteAction.create(timeoutNano, null, weightedClusters); + } + + private static RouteAction create(@Nullable Long timeoutNano, @Nullable String cluster, + @Nullable List weightedClusters) { + return new AutoValue_VirtualHost_Route_RouteAction(timeoutNano, cluster, + weightedClusters == null ? null : ImmutableList.copyOf(weightedClusters)); + } + + @AutoValue + abstract static class ClusterWeight { + abstract String name(); + + abstract int weight(); + + @Nullable + abstract HttpFault httpFault(); + + static ClusterWeight create(String name, int weight, @Nullable HttpFault httpFault) { + return new AutoValue_VirtualHost_Route_RouteAction_ClusterWeight(name, weight, + httpFault); + } + } + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/XdsClient.java b/xds/src/main/java/io/grpc/xds/XdsClient.java index ef3d90133f..4360468db7 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClient.java +++ b/xds/src/main/java/io/grpc/xds/XdsClient.java @@ -22,11 +22,8 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects.ToStringHelper; import io.grpc.Status; -import io.grpc.xds.EnvoyProtoData.DropOverload; -import io.grpc.xds.EnvoyProtoData.HttpFault; -import io.grpc.xds.EnvoyProtoData.Locality; -import io.grpc.xds.EnvoyProtoData.LocalityLbEndpoints; -import io.grpc.xds.EnvoyProtoData.VirtualHost; +import io.grpc.xds.Endpoints.DropOverload; +import io.grpc.xds.Endpoints.LocalityLbEndpoints; import io.grpc.xds.EnvoyServerProtoData.Listener; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.LoadStatsManager2.ClusterDropStats; diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index a84e35f333..953a66a285 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Joiner; import com.google.common.collect.Sets; import com.google.gson.Gson; import io.grpc.Attributes; @@ -39,11 +40,14 @@ import io.grpc.Status; import io.grpc.SynchronizationContext; import io.grpc.internal.GrpcUtil; import io.grpc.internal.ObjectPool; -import io.grpc.xds.EnvoyProtoData.ClusterWeight; -import io.grpc.xds.EnvoyProtoData.Route; -import io.grpc.xds.EnvoyProtoData.RouteAction; -import io.grpc.xds.EnvoyProtoData.VirtualHost; +import io.grpc.xds.Matchers.FractionMatcher; +import io.grpc.xds.Matchers.HeaderMatcher; +import io.grpc.xds.Matchers.PathMatcher; import io.grpc.xds.ThreadSafeRandom.ThreadSafeRandomImpl; +import io.grpc.xds.VirtualHost.Route; +import io.grpc.xds.VirtualHost.Route.RouteAction; +import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight; +import io.grpc.xds.VirtualHost.Route.RouteMatch; import io.grpc.xds.XdsClient.LdsResourceWatcher; import io.grpc.xds.XdsClient.LdsUpdate; import io.grpc.xds.XdsClient.RdsResourceWatcher; @@ -218,7 +222,7 @@ final class XdsNameResolver extends NameResolver { boolean exactMatchFound = false; // true if a virtual host with exactly matched domain found VirtualHost targetVirtualHost = null; // target VirtualHost with longest matched domain for (VirtualHost vHost : virtualHosts) { - for (String domain : vHost.getDomains()) { + for (String domain : vHost.domains()) { boolean selected = false; if (matchHostName(hostName, domain)) { // matching if (!domain.contains("*")) { // exact matching @@ -320,8 +324,8 @@ final class XdsNameResolver extends NameResolver { Route selectedRoute = null; do { for (Route route : routingConfig.routes) { - if (route.getRouteMatch().matches( - "/" + args.getMethodDescriptor().getFullMethodName(), asciiHeaders)) { + if (matchRoute(route.routeMatch(), "/" + args.getMethodDescriptor().getFullMethodName(), + asciiHeaders, random)) { selectedRoute = route; break; } @@ -330,20 +334,20 @@ final class XdsNameResolver extends NameResolver { return Result.forError( Status.UNAVAILABLE.withDescription("Could not find xDS route matching RPC")); } - RouteAction action = selectedRoute.getRouteAction(); - if (action.getCluster() != null) { - cluster = action.getCluster(); - } else if (action.getWeightedCluster() != null) { + RouteAction action = selectedRoute.routeAction(); + if (action.cluster() != null) { + cluster = action.cluster(); + } else if (action.weightedClusters() != null) { int totalWeight = 0; - for (ClusterWeight weightedCluster : action.getWeightedCluster()) { - totalWeight += weightedCluster.getWeight(); + for (ClusterWeight weightedCluster : action.weightedClusters()) { + totalWeight += weightedCluster.weight(); } int select = random.nextInt(totalWeight); int accumulator = 0; - for (ClusterWeight weightedCluster : action.getWeightedCluster()) { - accumulator += weightedCluster.getWeight(); + for (ClusterWeight weightedCluster : action.weightedClusters()) { + accumulator += weightedCluster.weight(); if (select < accumulator) { - cluster = weightedCluster.getName(); + cluster = weightedCluster.name(); break; } } @@ -352,7 +356,7 @@ final class XdsNameResolver extends NameResolver { // TODO(chengyuanzhang): avoid service config generation and parsing for each call. Map rawServiceConfig = Collections.emptyMap(); if (enableTimeout) { - Long timeoutNano = selectedRoute.getRouteAction().getTimeoutNano(); + Long timeoutNano = selectedRoute.routeAction().timeoutNano(); if (timeoutNano == null) { timeoutNano = routingConfig.fallbackTimeoutNano; } @@ -442,6 +446,76 @@ final class XdsNameResolver extends NameResolver { } } + @VisibleForTesting + static boolean matchRoute(RouteMatch routeMatch, String fullMethodName, + Map> headers, ThreadSafeRandom random) { + if (!matchPath(routeMatch.pathMatcher(), fullMethodName)) { + return false; + } + for (HeaderMatcher headerMatcher : routeMatch.headerMatchers()) { + Iterable headerValues = headers.get(headerMatcher.name()); + // Special cases for hiding headers: "grpc-previous-rpc-attempts". + if (headerMatcher.name().equals("grpc-previous-rpc-attempts")) { + headerValues = null; + } + // Special case for exposing headers: "content-type". + if (headerMatcher.name().equals("content-type")) { + headerValues = Collections.singletonList("application/grpc"); + } + if (!matchHeader(headerMatcher, headerValues)) { + return false; + } + } + FractionMatcher fraction = routeMatch.fractionMatcher(); + return fraction == null || random.nextInt(fraction.denominator()) < fraction.numerator(); + } + + @VisibleForTesting + static boolean matchPath(PathMatcher pathMatcher, String fullMethodName) { + if (pathMatcher.path() != null) { + return pathMatcher.caseSensitive() + ? pathMatcher.path().equals(fullMethodName) + : pathMatcher.path().equalsIgnoreCase(fullMethodName); + } else if (pathMatcher.prefix() != null) { + return pathMatcher.caseSensitive() + ? fullMethodName.startsWith(pathMatcher.prefix()) + : fullMethodName.toLowerCase().startsWith(pathMatcher.prefix().toLowerCase()); + } + return pathMatcher.regEx().matches(fullMethodName); + } + + @VisibleForTesting + static boolean matchHeader(HeaderMatcher headerMatcher, + @Nullable Iterable headerValues) { + if (headerMatcher.present() != null) { + return (headerValues == null) == headerMatcher.present().equals(headerMatcher.inverted()); + } + if (headerValues == null) { + return false; + } + String valueStr = Joiner.on(",").join(headerValues); + boolean baseMatch; + if (headerMatcher.exactValue() != null) { + baseMatch = headerMatcher.exactValue().equals(valueStr); + } else if (headerMatcher.safeRegEx() != null) { + baseMatch = headerMatcher.safeRegEx().matches(valueStr); + } else if (headerMatcher.range() != null) { + long numValue; + try { + numValue = Long.parseLong(valueStr); + baseMatch = numValue >= headerMatcher.range().start() + && numValue <= headerMatcher.range().end(); + } catch (NumberFormatException ignored) { + baseMatch = false; + } + } else if (headerMatcher.prefix() != null) { + baseMatch = valueStr.startsWith(headerMatcher.prefix()); + } else { + baseMatch = valueStr.endsWith(headerMatcher.suffix()); + } + return baseMatch != headerMatcher.inverted(); + } + private class ResolveState implements LdsResourceWatcher { private final ConfigOrError emptyServiceConfig = serviceConfigParser.parseServiceConfig(Collections.emptyMap()); @@ -537,15 +611,15 @@ final class XdsNameResolver extends NameResolver { listener.onResult(emptyResult); return; } - List routes = virtualHost.getRoutes(); + List routes = virtualHost.routes(); Set clusters = new HashSet<>(); for (Route route : routes) { - RouteAction action = route.getRouteAction(); - if (action.getCluster() != null) { - clusters.add(action.getCluster()); - } else if (action.getWeightedCluster() != null) { - for (ClusterWeight weighedCluster : action.getWeightedCluster()) { - clusters.add(weighedCluster.getName()); + RouteAction action = route.routeAction(); + if (action.cluster() != null) { + clusters.add(action.cluster()); + } else if (action.weightedClusters() != null) { + for (ClusterWeight weighedCluster : action.weightedClusters()) { + clusters.add(weighedCluster.name()); } } } diff --git a/xds/src/test/java/io/grpc/xds/BootstrapperImplTest.java b/xds/src/test/java/io/grpc/xds/BootstrapperImplTest.java index 1f344a6b50..6a69a79d97 100644 --- a/xds/src/test/java/io/grpc/xds/BootstrapperImplTest.java +++ b/xds/src/test/java/io/grpc/xds/BootstrapperImplTest.java @@ -29,7 +29,6 @@ import io.grpc.internal.GrpcUtil; import io.grpc.internal.GrpcUtil.GrpcBuildVersion; import io.grpc.xds.Bootstrapper.BootstrapInfo; import io.grpc.xds.Bootstrapper.ServerInfo; -import io.grpc.xds.EnvoyProtoData.Locality; import io.grpc.xds.EnvoyProtoData.Node; import java.io.IOException; import java.util.List; @@ -118,7 +117,7 @@ public class BootstrapperImplTest { getNodeBuilder() .setId("ENVOY_NODE_ID") .setCluster("ENVOY_CLUSTER") - .setLocality(new Locality("ENVOY_REGION", "ENVOY_ZONE", "ENVOY_SUBZONE")) + .setLocality(Locality.create("ENVOY_REGION", "ENVOY_ZONE", "ENVOY_SUBZONE")) .setMetadata( ImmutableMap.of( "TRAFFICDIRECTOR_INTERCEPTION_PORT", @@ -176,7 +175,7 @@ public class BootstrapperImplTest { getNodeBuilder() .setId("ENVOY_NODE_ID") .setCluster("ENVOY_CLUSTER") - .setLocality(new Locality("ENVOY_REGION", "ENVOY_ZONE", "ENVOY_SUBZONE")) + .setLocality(Locality.create("ENVOY_REGION", "ENVOY_ZONE", "ENVOY_SUBZONE")) .setMetadata( ImmutableMap.of( "TRAFFICDIRECTOR_INTERCEPTION_PORT", @@ -224,7 +223,7 @@ public class BootstrapperImplTest { getNodeBuilder() .setId("ENVOY_NODE_ID") .setCluster("ENVOY_CLUSTER") - .setLocality(new Locality("ENVOY_REGION", "ENVOY_ZONE", "ENVOY_SUBZONE")) + .setLocality(Locality.create("ENVOY_REGION", "ENVOY_ZONE", "ENVOY_SUBZONE")) .setMetadata( ImmutableMap.of( "TRAFFICDIRECTOR_INTERCEPTION_PORT", diff --git a/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java b/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java new file mode 100644 index 0000000000..27daa2d635 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java @@ -0,0 +1,598 @@ +/* + * Copyright 2021 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 com.google.protobuf.UInt32Value; +import com.google.protobuf.util.Durations; +import com.google.re2j.Pattern; +import io.envoyproxy.envoy.config.core.v3.Address; +import io.envoyproxy.envoy.config.core.v3.Locality; +import io.envoyproxy.envoy.config.core.v3.RuntimeFractionalPercent; +import io.envoyproxy.envoy.config.core.v3.SocketAddress; +import io.envoyproxy.envoy.config.endpoint.v3.Endpoint; +import io.envoyproxy.envoy.config.route.v3.DirectResponseAction; +import io.envoyproxy.envoy.config.route.v3.FilterAction; +import io.envoyproxy.envoy.config.route.v3.RedirectAction; +import io.envoyproxy.envoy.config.route.v3.RouteAction.MaxStreamDuration; +import io.envoyproxy.envoy.config.route.v3.WeightedCluster; +import io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort.HeaderAbort; +import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher; +import io.envoyproxy.envoy.type.v3.FractionalPercent; +import io.envoyproxy.envoy.type.v3.FractionalPercent.DenominatorType; +import io.envoyproxy.envoy.type.v3.Int64Range; +import io.grpc.Status.Code; +import io.grpc.xds.ClientXdsClient.StructOrError; +import io.grpc.xds.Endpoints.LbEndpoint; +import io.grpc.xds.Endpoints.LocalityLbEndpoints; +import io.grpc.xds.HttpFault.FaultAbort; +import io.grpc.xds.Matchers.FractionMatcher; +import io.grpc.xds.Matchers.HeaderMatcher; +import io.grpc.xds.Matchers.PathMatcher; +import io.grpc.xds.VirtualHost.Route; +import io.grpc.xds.VirtualHost.Route.RouteAction; +import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight; +import io.grpc.xds.VirtualHost.Route.RouteMatch; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ClientXdsClientDataTest { + + @Test + public void parseRoute_withRouteAction() { + io.envoyproxy.envoy.config.route.v3.Route proto = + io.envoyproxy.envoy.config.route.v3.Route.newBuilder() + .setName("route-blade") + .setMatch( + io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder() + .setPath("/service/method")) + .setRoute( + io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setCluster("cluster-foo")) + .build(); + StructOrError struct = ClientXdsClient.parseRoute(proto); + assertThat(struct.getErrorDetail()).isNull(); + assertThat(struct.getStruct()) + .isEqualTo( + Route.create( + RouteMatch.create(PathMatcher.fromPath("/service/method", false), + Collections.emptyList(), null), + RouteAction.forCluster("cluster-foo", null), null)); + } + + @Test + public void parseRoute_withUnsupportedActionTypes() { + StructOrError res; + io.envoyproxy.envoy.config.route.v3.Route redirectRoute = + io.envoyproxy.envoy.config.route.v3.Route.newBuilder() + .setName("route-blade") + .setMatch(io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder().setPath("")) + .setRedirect(RedirectAction.getDefaultInstance()) + .build(); + res = ClientXdsClient.parseRoute(redirectRoute); + assertThat(res.getStruct()).isNull(); + assertThat(res.getErrorDetail()).isEqualTo("Unsupported action type: redirect"); + + io.envoyproxy.envoy.config.route.v3.Route directResponseRoute = + io.envoyproxy.envoy.config.route.v3.Route.newBuilder() + .setName("route-blade") + .setMatch(io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder().setPath("")) + .setDirectResponse(DirectResponseAction.getDefaultInstance()) + .build(); + res = ClientXdsClient.parseRoute(directResponseRoute); + assertThat(res.getStruct()).isNull(); + assertThat(res.getErrorDetail()).isEqualTo("Unsupported action type: direct_response"); + + io.envoyproxy.envoy.config.route.v3.Route filterRoute = + io.envoyproxy.envoy.config.route.v3.Route.newBuilder() + .setName("route-blade") + .setMatch(io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder().setPath("")) + .setFilterAction(FilterAction.getDefaultInstance()) + .build(); + res = ClientXdsClient.parseRoute(filterRoute); + assertThat(res.getStruct()).isNull(); + assertThat(res.getErrorDetail()).isEqualTo("Unsupported action type: filter_action"); + } + + @Test + public void parseRoute_skipRouteWithUnsupportedMatcher() { + io.envoyproxy.envoy.config.route.v3.Route proto = + io.envoyproxy.envoy.config.route.v3.Route.newBuilder() + .setName("ignore me") + .setMatch( + io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder() + .setPath("/service/method") + .addQueryParameters( + io.envoyproxy.envoy.config.route.v3.QueryParameterMatcher + .getDefaultInstance())) // query parameter not supported + .setRoute( + io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setCluster("cluster-foo")) + .build(); + assertThat(ClientXdsClient.parseRoute(proto)).isNull(); + } + + @Test + public void parseRoute_skipRouteWithUnsupportedAction() { + io.envoyproxy.envoy.config.route.v3.Route proto = + io.envoyproxy.envoy.config.route.v3.Route.newBuilder() + .setName("ignore me") + .setMatch( + io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder() + .setPath("/service/method")) + .setRoute( + io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setClusterHeader("cluster header")) // cluster_header action not supported + .build(); + assertThat(ClientXdsClient.parseRoute(proto)).isNull(); + } + + @Test + public void parseRouteMatch_withHeaderMatcher() { + io.envoyproxy.envoy.config.route.v3.RouteMatch proto = + io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder() + .setPrefix("") + .addHeaders( + io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() + .setName(":scheme") + .setPrefixMatch("http")) + .addHeaders( + io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() + .setName(":method") + .setExactMatch("PUT")) + .build(); + StructOrError struct = ClientXdsClient.parseRouteMatch(proto); + assertThat(struct.getErrorDetail()).isNull(); + assertThat(struct.getStruct()) + .isEqualTo( + RouteMatch.create( + PathMatcher.fromPrefix("", false), + Arrays.asList( + HeaderMatcher.forPrefix(":scheme", "http", false), + HeaderMatcher.forExactValue(":method", "PUT", false)), + null)); + } + + @Test + public void parseRouteMatch_withRuntimeFractionMatcher() { + io.envoyproxy.envoy.config.route.v3.RouteMatch proto = + io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder() + .setPrefix("") + .setRuntimeFraction( + RuntimeFractionalPercent.newBuilder() + .setDefaultValue( + FractionalPercent.newBuilder() + .setNumerator(30) + .setDenominator(FractionalPercent.DenominatorType.HUNDRED))) + .build(); + StructOrError struct = ClientXdsClient.parseRouteMatch(proto); + assertThat(struct.getErrorDetail()).isNull(); + assertThat(struct.getStruct()) + .isEqualTo( + RouteMatch.create( + PathMatcher.fromPrefix( "", false), Collections.emptyList(), + FractionMatcher.create(30, 100))); + } + + @Test + public void parsePathMatcher_withFullPath() { + io.envoyproxy.envoy.config.route.v3.RouteMatch proto = + io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder() + .setPath("/service/method") + .build(); + StructOrError struct = ClientXdsClient.parsePathMatcher(proto); + assertThat(struct.getErrorDetail()).isNull(); + assertThat(struct.getStruct()).isEqualTo( + PathMatcher.fromPath("/service/method", false)); + } + + @Test + public void parsePathMatcher_withPrefix() { + io.envoyproxy.envoy.config.route.v3.RouteMatch proto = + io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder().setPrefix("/").build(); + StructOrError struct = ClientXdsClient.parsePathMatcher(proto); + assertThat(struct.getErrorDetail()).isNull(); + assertThat(struct.getStruct()).isEqualTo( + PathMatcher.fromPrefix("/", false)); + } + + @Test + public void parsePathMatcher_withSafeRegEx() { + io.envoyproxy.envoy.config.route.v3.RouteMatch proto = + io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder() + .setSafeRegex(RegexMatcher.newBuilder().setRegex(".")) + .build(); + StructOrError struct = ClientXdsClient.parsePathMatcher(proto); + assertThat(struct.getErrorDetail()).isNull(); + assertThat(struct.getStruct()).isEqualTo(PathMatcher.fromRegEx(Pattern.compile("."))); + } + + @Test + public void parseHeaderMatcher_withExactMatch() { + io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto = + io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() + .setName(":method") + .setExactMatch("PUT") + .build(); + StructOrError struct1 = ClientXdsClient.parseHeaderMatcher(proto); + assertThat(struct1.getErrorDetail()).isNull(); + assertThat(struct1.getStruct()).isEqualTo( + HeaderMatcher.forExactValue(":method", "PUT", false)); + } + + @Test + public void parseHeaderMatcher_withSafeRegExMatch() { + io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto = + io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() + .setName(":method") + .setSafeRegexMatch(RegexMatcher.newBuilder().setRegex("P*")) + .build(); + StructOrError struct3 = ClientXdsClient.parseHeaderMatcher(proto); + assertThat(struct3.getErrorDetail()).isNull(); + assertThat(struct3.getStruct()).isEqualTo( + HeaderMatcher.forSafeRegEx(":method", Pattern.compile("P*"), false)); + } + + @Test + public void parseHeaderMatcher_withRangeMatch() { + io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto = + io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() + .setName("timeout") + .setRangeMatch(Int64Range.newBuilder().setStart(10L).setEnd(20L)) + .build(); + StructOrError struct4 = ClientXdsClient.parseHeaderMatcher(proto); + assertThat(struct4.getErrorDetail()).isNull(); + assertThat(struct4.getStruct()).isEqualTo( + HeaderMatcher.forRange("timeout", HeaderMatcher.Range.create(10L, 20L), false)); + } + + @Test + public void parseHeaderMatcher_withPresentMatch() { + io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto = + io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() + .setName("user-agent") + .setPresentMatch(true) + .build(); + StructOrError struct5 = ClientXdsClient.parseHeaderMatcher(proto); + assertThat(struct5.getErrorDetail()).isNull(); + assertThat(struct5.getStruct()).isEqualTo( + HeaderMatcher.forPresent("user-agent", true, false)); + } + + @Test + public void parseHeaderMatcher_withPrefixMatch() { + io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto = + io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() + .setName("authority") + .setPrefixMatch("service-foo") + .build(); + StructOrError struct6 = ClientXdsClient.parseHeaderMatcher(proto); + assertThat(struct6.getErrorDetail()).isNull(); + assertThat(struct6.getStruct()).isEqualTo( + HeaderMatcher.forPrefix("authority", "service-foo", false)); + } + + @Test + public void parseHeaderMatcher_withSuffixMatch() { + io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto = + io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() + .setName("authority") + .setSuffixMatch("googleapis.com") + .build(); + StructOrError struct7 = ClientXdsClient.parseHeaderMatcher(proto); + assertThat(struct7.getErrorDetail()).isNull(); + assertThat(struct7.getStruct()).isEqualTo( + HeaderMatcher.forSuffix("authority", "googleapis.com", false)); + } + + @Test + public void parseHeaderMatcher_malformedRegExPattern() { + io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto = + io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() + .setName(":method") + .setSafeRegexMatch(RegexMatcher.newBuilder().setRegex("[")) + .build(); + StructOrError struct = ClientXdsClient.parseHeaderMatcher(proto); + assertThat(struct.getErrorDetail()).isNotNull(); + assertThat(struct.getStruct()).isNull(); + } + + @Test + public void parseRouteAction_withCluster() { + io.envoyproxy.envoy.config.route.v3.RouteAction proto = + io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setCluster("cluster-foo") + .build(); + StructOrError struct = ClientXdsClient.parseRouteAction(proto); + assertThat(struct.getErrorDetail()).isNull(); + assertThat(struct.getStruct().cluster()).isEqualTo("cluster-foo"); + assertThat(struct.getStruct().weightedClusters()).isNull(); + } + + @Test + public void parseRouteAction_withWeightedCluster() { + io.envoyproxy.envoy.config.route.v3.RouteAction proto = + io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setWeightedClusters( + WeightedCluster.newBuilder() + .addClusters( + WeightedCluster.ClusterWeight + .newBuilder() + .setName("cluster-foo") + .setWeight(UInt32Value.newBuilder().setValue(30))) + .addClusters(WeightedCluster.ClusterWeight + .newBuilder() + .setName("cluster-bar") + .setWeight(UInt32Value.newBuilder().setValue(70)))) + .build(); + StructOrError struct = ClientXdsClient.parseRouteAction(proto); + assertThat(struct.getErrorDetail()).isNull(); + assertThat(struct.getStruct().cluster()).isNull(); + assertThat(struct.getStruct().weightedClusters()).containsExactly( + ClusterWeight.create("cluster-foo", 30, null), + ClusterWeight.create("cluster-bar", 70, null)); + } + + @Test + public void parseRouteAction_withTimeoutByGrpcTimeoutHeaderMax() { + io.envoyproxy.envoy.config.route.v3.RouteAction proto = + io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setCluster("cluster-foo") + .setMaxStreamDuration( + MaxStreamDuration.newBuilder() + .setGrpcTimeoutHeaderMax(Durations.fromSeconds(5L)) + .setMaxStreamDuration(Durations.fromMillis(20L))) + .build(); + StructOrError struct = ClientXdsClient.parseRouteAction(proto); + assertThat(struct.getStruct().timeoutNano()).isEqualTo(TimeUnit.SECONDS.toNanos(5L)); + } + + @Test + public void parseRouteAction_withTimeoutByMaxStreamDuration() { + io.envoyproxy.envoy.config.route.v3.RouteAction proto = + io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setCluster("cluster-foo") + .setMaxStreamDuration( + MaxStreamDuration.newBuilder() + .setMaxStreamDuration(Durations.fromSeconds(5L))) + .build(); + StructOrError struct = ClientXdsClient.parseRouteAction(proto); + assertThat(struct.getStruct().timeoutNano()).isEqualTo(TimeUnit.SECONDS.toNanos(5L)); + } + + @Test + public void parseRouteAction_withTimeoutUnset() { + io.envoyproxy.envoy.config.route.v3.RouteAction proto = + io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setCluster("cluster-foo") + .build(); + StructOrError struct = ClientXdsClient.parseRouteAction(proto); + assertThat(struct.getStruct().timeoutNano()).isNull(); + } + + @Test + public void parseClusterWeight() { + io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight proto = + io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight.newBuilder() + .setName("cluster-foo") + .setWeight(UInt32Value.newBuilder().setValue(30)) + .build(); + ClusterWeight clusterWeight = ClientXdsClient.parseClusterWeight(proto).getStruct(); + assertThat(clusterWeight.name()).isEqualTo("cluster-foo"); + assertThat(clusterWeight.weight()).isEqualTo(30); + } + + // TODO(zdapeng): add tests for parseClusterWeight with HttpFault. + + // TODO(zdapeng): add tests for parseHttpFault. + + @Test + public void parseFaultAbort_withHeaderAbort() { + io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort proto = + io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort.newBuilder() + .setPercentage(FractionalPercent.newBuilder() + .setNumerator(20).setDenominator(DenominatorType.HUNDRED)) + .setHeaderAbort(HeaderAbort.getDefaultInstance()).build(); + FaultAbort faultAbort = ClientXdsClient.parseFaultAbort(proto).getStruct(); + assertThat(faultAbort.headerAbort()).isTrue(); + assertThat(faultAbort.ratePerMillion()).isEqualTo(200_000); + } + + @Test + public void parseFaultAbort_withHttpStatus() { + FaultAbort res; + io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort proto; + proto = io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort.newBuilder() + .setPercentage(FractionalPercent.newBuilder() + .setNumerator(100).setDenominator(DenominatorType.TEN_THOUSAND)) + .setHttpStatus(400).build(); + res = ClientXdsClient.parseFaultAbort(proto).getStruct(); + assertThat(res.ratePerMillion()).isEqualTo(10_000); + assertThat(res.status().getCode()).isEqualTo(Code.INTERNAL); + + proto = io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort.newBuilder() + .setPercentage(FractionalPercent.newBuilder() + .setNumerator(100).setDenominator(DenominatorType.TEN_THOUSAND)) + .setHttpStatus(401).build(); + res = ClientXdsClient.parseFaultAbort(proto).getStruct(); + assertThat(res.ratePerMillion()).isEqualTo(10_000); + assertThat(res.status().getCode()).isEqualTo(Code.UNAUTHENTICATED); + + proto = io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort.newBuilder() + .setPercentage(FractionalPercent.newBuilder() + .setNumerator(100).setDenominator(DenominatorType.TEN_THOUSAND)) + .setHttpStatus(403).build(); + res = ClientXdsClient.parseFaultAbort(proto).getStruct(); + assertThat(res.ratePerMillion()).isEqualTo(10_000); + assertThat(res.status().getCode()).isEqualTo(Code.PERMISSION_DENIED); + + proto = io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort.newBuilder() + .setPercentage(FractionalPercent.newBuilder() + .setNumerator(100).setDenominator(DenominatorType.TEN_THOUSAND)) + .setHttpStatus(404).build(); + res = ClientXdsClient.parseFaultAbort(proto).getStruct(); + assertThat(res.ratePerMillion()).isEqualTo(10_000); + assertThat(res.status().getCode()).isEqualTo(Code.UNIMPLEMENTED); + + proto = io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort.newBuilder() + .setPercentage(FractionalPercent.newBuilder() + .setNumerator(100).setDenominator(DenominatorType.TEN_THOUSAND)) + .setHttpStatus(503).build(); + res = ClientXdsClient.parseFaultAbort(proto).getStruct(); + assertThat(res.ratePerMillion()).isEqualTo(10_000); + assertThat(res.status().getCode()).isEqualTo(Code.UNAVAILABLE); + + proto = io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort.newBuilder() + .setPercentage(FractionalPercent.newBuilder() + .setNumerator(100).setDenominator(DenominatorType.TEN_THOUSAND)) + .setHttpStatus(500).build(); + res = ClientXdsClient.parseFaultAbort(proto).getStruct(); + assertThat(res.ratePerMillion()).isEqualTo(10_000); + assertThat(res.status().getCode()).isEqualTo(Code.UNKNOWN); + } + + @Test + public void parseFaultAbort_withGrpcStatus() { + io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort proto = + io.envoyproxy.envoy.extensions.filters.http.fault.v3.FaultAbort.newBuilder() + .setPercentage(FractionalPercent.newBuilder() + .setNumerator(600).setDenominator(DenominatorType.MILLION)) + .setGrpcStatus(Code.DEADLINE_EXCEEDED.value()).build(); + FaultAbort faultAbort = ClientXdsClient.parseFaultAbort(proto).getStruct(); + assertThat(faultAbort.ratePerMillion()).isEqualTo(600); + assertThat(faultAbort.status().getCode()).isEqualTo(Code.DEADLINE_EXCEEDED); + } + + @Test + public void parseLocalityLbEndpoints_withHealthyEndpoints() { + io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto = + io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints.newBuilder() + .setLocality(Locality.newBuilder() + .setRegion("region-foo").setZone("zone-foo").setSubZone("subZone-foo")) + .setLoadBalancingWeight(UInt32Value.newBuilder().setValue(100)) // locality weight + .setPriority(1) + .addLbEndpoints(io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint.newBuilder() + .setEndpoint(Endpoint.newBuilder() + .setAddress(Address.newBuilder() + .setSocketAddress( + SocketAddress.newBuilder() + .setAddress("172.14.14.5").setPortValue(8888)))) + .setHealthStatus(io.envoyproxy.envoy.config.core.v3.HealthStatus.HEALTHY) + .setLoadBalancingWeight(UInt32Value.newBuilder().setValue(20))) // endpoint weight + .build(); + StructOrError struct = ClientXdsClient.parseLocalityLbEndpoints(proto); + assertThat(struct.getErrorDetail()).isNull(); + assertThat(struct.getStruct()).isEqualTo( + LocalityLbEndpoints.create( + Collections.singletonList(LbEndpoint.create("172.14.14.5", 8888, 20, true)), 100, 1)); + } + + @Test + public void parseLocalityLbEndpoints_treatUnknownHealthAsHealthy() { + io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto = + io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints.newBuilder() + .setLocality(Locality.newBuilder() + .setRegion("region-foo").setZone("zone-foo").setSubZone("subZone-foo")) + .setLoadBalancingWeight(UInt32Value.newBuilder().setValue(100)) // locality weight + .setPriority(1) + .addLbEndpoints(io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint.newBuilder() + .setEndpoint(Endpoint.newBuilder() + .setAddress(Address.newBuilder() + .setSocketAddress( + SocketAddress.newBuilder() + .setAddress("172.14.14.5").setPortValue(8888)))) + .setHealthStatus(io.envoyproxy.envoy.config.core.v3.HealthStatus.UNKNOWN) + .setLoadBalancingWeight(UInt32Value.newBuilder().setValue(20))) // endpoint weight + .build(); + StructOrError struct = ClientXdsClient.parseLocalityLbEndpoints(proto); + assertThat(struct.getErrorDetail()).isNull(); + assertThat(struct.getStruct()).isEqualTo( + LocalityLbEndpoints.create( + Collections.singletonList(LbEndpoint.create("172.14.14.5", 8888, 20, true)), 100, 1)); + } + + @Test + public void parseLocalityLbEndpoints_withUnHealthyEndpoints() { + io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto = + io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints.newBuilder() + .setLocality(Locality.newBuilder() + .setRegion("region-foo").setZone("zone-foo").setSubZone("subZone-foo")) + .setLoadBalancingWeight(UInt32Value.newBuilder().setValue(100)) // locality weight + .setPriority(1) + .addLbEndpoints(io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint.newBuilder() + .setEndpoint(Endpoint.newBuilder() + .setAddress(Address.newBuilder() + .setSocketAddress( + SocketAddress.newBuilder() + .setAddress("172.14.14.5").setPortValue(8888)))) + .setHealthStatus(io.envoyproxy.envoy.config.core.v3.HealthStatus.UNHEALTHY) + .setLoadBalancingWeight(UInt32Value.newBuilder().setValue(20))) // endpoint weight + .build(); + StructOrError struct = ClientXdsClient.parseLocalityLbEndpoints(proto); + assertThat(struct.getErrorDetail()).isNull(); + assertThat(struct.getStruct()).isEqualTo( + LocalityLbEndpoints.create( + Collections.singletonList(LbEndpoint.create("172.14.14.5", 8888, 20, false)), 100, 1)); + } + + @Test + public void parseLocalityLbEndpoints_ignorZeroWeightLocality() { + io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto = + io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints.newBuilder() + .setLocality(Locality.newBuilder() + .setRegion("region-foo").setZone("zone-foo").setSubZone("subZone-foo")) + .setLoadBalancingWeight(UInt32Value.newBuilder().setValue(0)) // locality weight + .setPriority(1) + .addLbEndpoints(io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint.newBuilder() + .setEndpoint(Endpoint.newBuilder() + .setAddress(Address.newBuilder() + .setSocketAddress( + SocketAddress.newBuilder() + .setAddress("172.14.14.5").setPortValue(8888)))) + .setHealthStatus(io.envoyproxy.envoy.config.core.v3.HealthStatus.UNKNOWN) + .setLoadBalancingWeight(UInt32Value.newBuilder().setValue(20))) // endpoint weight + .build(); + assertThat(ClientXdsClient.parseLocalityLbEndpoints(proto)).isNull(); + } + + @Test + public void parseLocalityLbEndpoints_invalidPriority() { + io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints proto = + io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints.newBuilder() + .setLocality(Locality.newBuilder() + .setRegion("region-foo").setZone("zone-foo").setSubZone("subZone-foo")) + .setLoadBalancingWeight(UInt32Value.newBuilder().setValue(100)) // locality weight + .setPriority(-1) + .addLbEndpoints(io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint.newBuilder() + .setEndpoint(Endpoint.newBuilder() + .setAddress(Address.newBuilder() + .setSocketAddress( + SocketAddress.newBuilder() + .setAddress("172.14.14.5").setPortValue(8888)))) + .setHealthStatus(io.envoyproxy.envoy.config.core.v3.HealthStatus.UNKNOWN) + .setLoadBalancingWeight(UInt32Value.newBuilder().setValue(20))) // endpoint weight + .build(); + StructOrError struct = ClientXdsClient.parseLocalityLbEndpoints(proto); + assertThat(struct.getErrorDetail()).isEqualTo("negative priority"); + } +} diff --git a/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java b/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java index bfefcdd411..edc24b8180 100644 --- a/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java +++ b/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java @@ -43,11 +43,9 @@ import io.grpc.internal.FakeClock.ScheduledTask; import io.grpc.internal.FakeClock.TaskFilter; import io.grpc.testing.GrpcCleanupRule; import io.grpc.xds.AbstractXdsClient.ResourceType; -import io.grpc.xds.EnvoyProtoData.DropOverload; -import io.grpc.xds.EnvoyProtoData.HttpFault; -import io.grpc.xds.EnvoyProtoData.LbEndpoint; -import io.grpc.xds.EnvoyProtoData.Locality; -import io.grpc.xds.EnvoyProtoData.LocalityLbEndpoints; +import io.grpc.xds.Endpoints.DropOverload; +import io.grpc.xds.Endpoints.LbEndpoint; +import io.grpc.xds.Endpoints.LocalityLbEndpoints; import io.grpc.xds.EnvoyProtoData.Node; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.LoadStatsManager2.ClusterDropStats; @@ -374,18 +372,18 @@ public abstract class ClientXdsClientTestBase { assertThat(ldsUpdate.virtualHosts).hasSize(2); assertThat(ldsUpdate.hasFaultInjection).isTrue(); assertThat(ldsUpdate.httpFault).isNull(); - HttpFault httpFault = ldsUpdate.virtualHosts.get(0).getHttpFault(); - assertThat(httpFault.faultDelay.delayNanos).isEqualTo(300); - assertThat(httpFault.faultDelay.ratePerMillion).isEqualTo(1000); - assertThat(httpFault.faultAbort).isNull(); - assertThat(httpFault.upstreamCluster).isEqualTo("cluster1"); - assertThat(httpFault.maxActiveFaults).isEqualTo(100); - httpFault = ldsUpdate.virtualHosts.get(1).getHttpFault(); - assertThat(httpFault.faultDelay).isNull(); - assertThat(httpFault.faultAbort.status.getCode()).isEqualTo(Status.Code.UNAVAILABLE); - assertThat(httpFault.faultAbort.ratePerMillion).isEqualTo(2000); - assertThat(httpFault.upstreamCluster).isEqualTo("cluster2"); - assertThat(httpFault.maxActiveFaults).isEqualTo(101); + HttpFault httpFault = ldsUpdate.virtualHosts.get(0).httpFault(); + assertThat(httpFault.faultDelay().delayNanos()).isEqualTo(300); + assertThat(httpFault.faultDelay().ratePerMillion()).isEqualTo(1000); + assertThat(httpFault.faultAbort()).isNull(); + assertThat(httpFault.upstreamCluster()).isEqualTo("cluster1"); + assertThat(httpFault.maxActiveFaults()).isEqualTo(100); + httpFault = ldsUpdate.virtualHosts.get(1).httpFault(); + assertThat(httpFault.faultDelay()).isNull(); + assertThat(httpFault.faultAbort().status().getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(httpFault.faultAbort().ratePerMillion()).isEqualTo(2000); + assertThat(httpFault.upstreamCluster()).isEqualTo("cluster2"); + assertThat(httpFault.maxActiveFaults()).isEqualTo(101); } @Test @@ -942,17 +940,17 @@ public abstract class ClientXdsClientTestBase { assertThat(edsUpdate.clusterName).isEqualTo(EDS_RESOURCE); assertThat(edsUpdate.dropPolicies) .containsExactly( - new DropOverload("lb", 200), - new DropOverload("throttle", 1000)); + DropOverload.create("lb", 200), + DropOverload.create("throttle", 1000)); assertThat(edsUpdate.localityLbEndpointsMap) .containsExactly( - new Locality("region1", "zone1", "subzone1"), - new LocalityLbEndpoints( + Locality.create("region1", "zone1", "subzone1"), + LocalityLbEndpoints.create( ImmutableList.of( - new LbEndpoint("192.168.0.1", 8080, + LbEndpoint.create("192.168.0.1", 8080, 2, true)), 1, 0), - new Locality("region3", "zone3", "subzone3"), - new LocalityLbEndpoints(ImmutableList.of(), 2, 1)); + Locality.create("region3", "zone3", "subzone3"), + LocalityLbEndpoints.create(ImmutableList.of(), 2, 1)); } @Test @@ -991,17 +989,17 @@ public abstract class ClientXdsClientTestBase { assertThat(edsUpdate.clusterName).isEqualTo(EDS_RESOURCE); assertThat(edsUpdate.dropPolicies) .containsExactly( - new DropOverload("lb", 200), - new DropOverload("throttle", 1000)); + DropOverload.create("lb", 200), + DropOverload.create("throttle", 1000)); assertThat(edsUpdate.localityLbEndpointsMap) .containsExactly( - new Locality("region1", "zone1", "subzone1"), - new LocalityLbEndpoints( + Locality.create("region1", "zone1", "subzone1"), + LocalityLbEndpoints.create( ImmutableList.of( - new LbEndpoint("192.168.0.1", 8080, + LbEndpoint.create("192.168.0.1", 8080, 2, true)), 1, 0), - new Locality("region3", "zone3", "subzone3"), - new LocalityLbEndpoints(ImmutableList.of(), 2, 1)); + Locality.create("region3", "zone3", "subzone3"), + LocalityLbEndpoints.create(ImmutableList.of(), 2, 1)); call.verifyNoMoreRequest(); } @@ -1050,16 +1048,16 @@ public abstract class ClientXdsClientTestBase { assertThat(edsUpdate.clusterName).isEqualTo(EDS_RESOURCE); assertThat(edsUpdate.dropPolicies) .containsExactly( - new DropOverload("lb", 200), - new DropOverload("throttle", 1000)); + DropOverload.create("lb", 200), + DropOverload.create("throttle", 1000)); assertThat(edsUpdate.localityLbEndpointsMap) .containsExactly( - new Locality("region1", "zone1", "subzone1"), - new LocalityLbEndpoints( + Locality.create("region1", "zone1", "subzone1"), + LocalityLbEndpoints.create( ImmutableList.of( - new LbEndpoint("192.168.0.1", 8080, 2, true)), 1, 0), - new Locality("region3", "zone3", "subzone3"), - new LocalityLbEndpoints(ImmutableList.of(), 2, 1)); + LbEndpoint.create("192.168.0.1", 8080, 2, true)), 1, 0), + Locality.create("region3", "zone3", "subzone3"), + LocalityLbEndpoints.create(ImmutableList.of(), 2, 1)); clusterLoadAssignments = ImmutableList.of( @@ -1079,10 +1077,10 @@ public abstract class ClientXdsClientTestBase { assertThat(edsUpdate.dropPolicies).isEmpty(); assertThat(edsUpdate.localityLbEndpointsMap) .containsExactly( - new Locality("region2", "zone2", "subzone2"), - new LocalityLbEndpoints( + Locality.create("region2", "zone2", "subzone2"), + LocalityLbEndpoints.create( ImmutableList.of( - new LbEndpoint("172.44.2.2", 8000, 3, true)), 2, 0)); + LbEndpoint.create("172.44.2.2", 8000, 3, true)), 2, 0)); } @Test @@ -1187,16 +1185,16 @@ public abstract class ClientXdsClientTestBase { assertThat(edsUpdate.clusterName).isEqualTo(EDS_RESOURCE); assertThat(edsUpdate.dropPolicies) .containsExactly( - new DropOverload("lb", 200), - new DropOverload("throttle", 1000)); + DropOverload.create("lb", 200), + DropOverload.create("throttle", 1000)); assertThat(edsUpdate.localityLbEndpointsMap) .containsExactly( - new Locality("region1", "zone1", "subzone1"), - new LocalityLbEndpoints( + Locality.create("region1", "zone1", "subzone1"), + LocalityLbEndpoints.create( ImmutableList.of( - new LbEndpoint("192.168.0.1", 8080, 2, true)), 1, 0), - new Locality("region3", "zone3", "subzone3"), - new LocalityLbEndpoints(ImmutableList.of(), 2, 1)); + LbEndpoint.create("192.168.0.1", 8080, 2, true)), 1, 0), + Locality.create("region3", "zone3", "subzone3"), + LocalityLbEndpoints.create(ImmutableList.of(), 2, 1)); verifyNoMoreInteractions(watcher1, watcher2); clusterLoadAssignments = @@ -1217,20 +1215,20 @@ public abstract class ClientXdsClientTestBase { assertThat(edsUpdate.dropPolicies).isEmpty(); assertThat(edsUpdate.localityLbEndpointsMap) .containsExactly( - new Locality("region2", "zone2", "subzone2"), - new LocalityLbEndpoints( + Locality.create("region2", "zone2", "subzone2"), + LocalityLbEndpoints.create( ImmutableList.of( - new LbEndpoint("172.44.2.2", 8000, 3, true)), 2, 0)); + LbEndpoint.create("172.44.2.2", 8000, 3, true)), 2, 0)); verify(watcher2).onChanged(edsUpdateCaptor.capture()); edsUpdate = edsUpdateCaptor.getValue(); assertThat(edsUpdate.clusterName).isEqualTo(edsResource); assertThat(edsUpdate.dropPolicies).isEmpty(); assertThat(edsUpdate.localityLbEndpointsMap) .containsExactly( - new Locality("region2", "zone2", "subzone2"), - new LocalityLbEndpoints( + Locality.create("region2", "zone2", "subzone2"), + LocalityLbEndpoints.create( ImmutableList.of( - new LbEndpoint("172.44.2.2", 8000, 3, true)), 2, 0)); + LbEndpoint.create("172.44.2.2", 8000, 3, true)), 2, 0)); verifyNoMoreInteractions(edsResourceWatcher); } diff --git a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java index 959b16e7c4..6d3cf01b7c 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java @@ -45,14 +45,13 @@ import io.grpc.internal.FakeClock; import io.grpc.internal.ObjectPool; import io.grpc.internal.ServiceConfigUtil.PolicySelection; import io.grpc.xds.ClusterImplLoadBalancerProvider.ClusterImplConfig; -import io.grpc.xds.EnvoyProtoData.ClusterStats; -import io.grpc.xds.EnvoyProtoData.DropOverload; -import io.grpc.xds.EnvoyProtoData.Locality; -import io.grpc.xds.EnvoyProtoData.UpstreamLocalityStats; +import io.grpc.xds.Endpoints.DropOverload; import io.grpc.xds.EnvoyServerProtoData.DownstreamTlsContext; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.LoadStatsManager2.ClusterDropStats; import io.grpc.xds.LoadStatsManager2.ClusterLocalityStats; +import io.grpc.xds.Stats.ClusterStats; +import io.grpc.xds.Stats.UpstreamLocalityStats; import io.grpc.xds.WeightedTargetLoadBalancerProvider.WeightedPolicySelection; import io.grpc.xds.WeightedTargetLoadBalancerProvider.WeightedTargetConfig; import io.grpc.xds.XdsNameResolverProvider.CallCounterProvider; @@ -97,7 +96,7 @@ public class ClusterImplLoadBalancerTest { }); private final FakeClock fakeClock = new FakeClock(); private final Locality locality = - new Locality("test-region", "test-zone", "test-subzone"); + Locality.create("test-region", "test-zone", "test-subzone"); private final PolicySelection roundRobin = new PolicySelection(new FakeLoadBalancerProvider("round_robin"), null); private final List downstreamBalancers = new ArrayList<>(); @@ -219,27 +218,27 @@ public class ClusterImplLoadBalancerTest { ClusterStats clusterStats = Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER)); UpstreamLocalityStats localityStats = - Iterables.getOnlyElement(clusterStats.getUpstreamLocalityStatsList()); - assertThat(localityStats.getLocality()).isEqualTo(locality); - assertThat(localityStats.getTotalIssuedRequests()).isEqualTo(3L); - assertThat(localityStats.getTotalSuccessfulRequests()).isEqualTo(1L); - assertThat(localityStats.getTotalErrorRequests()).isEqualTo(1L); - assertThat(localityStats.getTotalRequestsInProgress()).isEqualTo(1L); + Iterables.getOnlyElement(clusterStats.upstreamLocalityStatsList()); + assertThat(localityStats.locality()).isEqualTo(locality); + assertThat(localityStats.totalIssuedRequests()).isEqualTo(3L); + assertThat(localityStats.totalSuccessfulRequests()).isEqualTo(1L); + assertThat(localityStats.totalErrorRequests()).isEqualTo(1L); + assertThat(localityStats.totalRequestsInProgress()).isEqualTo(1L); streamTracer3.streamClosed(Status.OK); subchannel.shutdown(); // stats recorder released clusterStats = Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER)); // Locality load is reported for one last time in case of loads occurred since the previous // load report. - localityStats = Iterables.getOnlyElement(clusterStats.getUpstreamLocalityStatsList()); - assertThat(localityStats.getLocality()).isEqualTo(locality); - assertThat(localityStats.getTotalIssuedRequests()).isEqualTo(0L); - assertThat(localityStats.getTotalSuccessfulRequests()).isEqualTo(1L); - assertThat(localityStats.getTotalErrorRequests()).isEqualTo(0L); - assertThat(localityStats.getTotalRequestsInProgress()).isEqualTo(0L); + localityStats = Iterables.getOnlyElement(clusterStats.upstreamLocalityStatsList()); + assertThat(localityStats.locality()).isEqualTo(locality); + assertThat(localityStats.totalIssuedRequests()).isEqualTo(0L); + assertThat(localityStats.totalSuccessfulRequests()).isEqualTo(1L); + assertThat(localityStats.totalErrorRequests()).isEqualTo(0L); + assertThat(localityStats.totalRequestsInProgress()).isEqualTo(0L); clusterStats = Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER)); - assertThat(clusterStats.getUpstreamLocalityStatsList()).isEmpty(); // no longer reported + assertThat(clusterStats.upstreamLocalityStatsList()).isEmpty(); // no longer reported } @Test @@ -248,7 +247,7 @@ public class ClusterImplLoadBalancerTest { WeightedTargetConfig weightedTargetConfig = buildWeightedTargetConfig(ImmutableMap.of(locality, 10)); ClusterImplConfig config = new ClusterImplConfig(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_NAME, - null, Collections.singletonList(new DropOverload("throttle", 500_000)), + null, Collections.singletonList(DropOverload.create("throttle", 500_000)), new PolicySelection(weightedTargetProvider, weightedTargetConfig), null); EquivalentAddressGroup endpoint = makeAddress("endpoint-addr", locality); deliverAddressesAndConfig(Collections.singletonList(endpoint), config); @@ -268,16 +267,16 @@ public class ClusterImplLoadBalancerTest { assertThat(result.getStatus().getDescription()).isEqualTo("Dropped: throttle"); ClusterStats clusterStats = Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER)); - assertThat(clusterStats.getClusterServiceName()).isEqualTo(EDS_SERVICE_NAME); - assertThat(Iterables.getOnlyElement(clusterStats.getDroppedRequestsList()).getCategory()) + assertThat(clusterStats.clusterServiceName()).isEqualTo(EDS_SERVICE_NAME); + assertThat(Iterables.getOnlyElement(clusterStats.droppedRequestsList()).category()) .isEqualTo("throttle"); - assertThat(Iterables.getOnlyElement(clusterStats.getDroppedRequestsList()).getDroppedCount()) + assertThat(Iterables.getOnlyElement(clusterStats.droppedRequestsList()).droppedCount()) .isEqualTo(1L); - assertThat(clusterStats.getTotalDroppedRequests()).isEqualTo(1L); + assertThat(clusterStats.totalDroppedRequests()).isEqualTo(1L); // Config update updates drop policies. config = new ClusterImplConfig(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_NAME, null, - Collections.singletonList(new DropOverload("lb", 1_000_000)), + Collections.singletonList(DropOverload.create("lb", 1_000_000)), new PolicySelection(weightedTargetProvider, weightedTargetConfig), null); loadBalancer.handleResolvedAddresses( ResolvedAddresses.newBuilder() @@ -294,12 +293,12 @@ public class ClusterImplLoadBalancerTest { assertThat(result.getStatus().getDescription()).isEqualTo("Dropped: lb"); clusterStats = Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER)); - assertThat(clusterStats.getClusterServiceName()).isEqualTo(EDS_SERVICE_NAME); - assertThat(Iterables.getOnlyElement(clusterStats.getDroppedRequestsList()).getCategory()) + assertThat(clusterStats.clusterServiceName()).isEqualTo(EDS_SERVICE_NAME); + assertThat(Iterables.getOnlyElement(clusterStats.droppedRequestsList()).category()) .isEqualTo("lb"); - assertThat(Iterables.getOnlyElement(clusterStats.getDroppedRequestsList()).getDroppedCount()) + assertThat(Iterables.getOnlyElement(clusterStats.droppedRequestsList()).droppedCount()) .isEqualTo(1L); - assertThat(clusterStats.getTotalDroppedRequests()).isEqualTo(1L); + assertThat(clusterStats.totalDroppedRequests()).isEqualTo(1L); result = currentPicker.pickSubchannel(mock(PickSubchannelArgs.class)); assertThat(result.getStatus().isOk()).isTrue(); @@ -346,21 +345,21 @@ public class ClusterImplLoadBalancerTest { } ClusterStats clusterStats = Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER)); - assertThat(clusterStats.getClusterServiceName()).isEqualTo(EDS_SERVICE_NAME); - assertThat(clusterStats.getTotalDroppedRequests()).isEqualTo(0L); + assertThat(clusterStats.clusterServiceName()).isEqualTo(EDS_SERVICE_NAME); + assertThat(clusterStats.totalDroppedRequests()).isEqualTo(0L); PickResult result = currentPicker.pickSubchannel(mock(PickSubchannelArgs.class)); clusterStats = Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER)); - assertThat(clusterStats.getClusterServiceName()).isEqualTo(EDS_SERVICE_NAME); + assertThat(clusterStats.clusterServiceName()).isEqualTo(EDS_SERVICE_NAME); if (enableCircuitBreaking) { assertThat(result.getStatus().isOk()).isFalse(); assertThat(result.getStatus().getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(result.getStatus().getDescription()) .isEqualTo("Cluster max concurrent requests limit exceeded"); - assertThat(clusterStats.getTotalDroppedRequests()).isEqualTo(1L); + assertThat(clusterStats.totalDroppedRequests()).isEqualTo(1L); } else { assertThat(result.getStatus().isOk()).isTrue(); - assertThat(clusterStats.getTotalDroppedRequests()).isEqualTo(0L); + assertThat(clusterStats.totalDroppedRequests()).isEqualTo(0L); } // Config update increments circuit breakers max_concurrent_requests threshold. @@ -375,21 +374,21 @@ public class ClusterImplLoadBalancerTest { result.getStreamTracerFactory().newClientStreamTracer( ClientStreamTracer.StreamInfo.newBuilder().build(), new Metadata()); // 101th request clusterStats = Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER)); - assertThat(clusterStats.getClusterServiceName()).isEqualTo(EDS_SERVICE_NAME); - assertThat(clusterStats.getTotalDroppedRequests()).isEqualTo(0L); + assertThat(clusterStats.clusterServiceName()).isEqualTo(EDS_SERVICE_NAME); + assertThat(clusterStats.totalDroppedRequests()).isEqualTo(0L); result = currentPicker.pickSubchannel(mock(PickSubchannelArgs.class)); // 102th request clusterStats = Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER)); - assertThat(clusterStats.getClusterServiceName()).isEqualTo(EDS_SERVICE_NAME); + assertThat(clusterStats.clusterServiceName()).isEqualTo(EDS_SERVICE_NAME); if (enableCircuitBreaking) { assertThat(result.getStatus().isOk()).isFalse(); assertThat(result.getStatus().getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(result.getStatus().getDescription()) .isEqualTo("Cluster max concurrent requests limit exceeded"); - assertThat(clusterStats.getTotalDroppedRequests()).isEqualTo(1L); + assertThat(clusterStats.totalDroppedRequests()).isEqualTo(1L); } else { assertThat(result.getStatus().isOk()).isTrue(); - assertThat(clusterStats.getTotalDroppedRequests()).isEqualTo(0L); + assertThat(clusterStats.totalDroppedRequests()).isEqualTo(0L); } } @@ -434,21 +433,21 @@ public class ClusterImplLoadBalancerTest { } ClusterStats clusterStats = Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER)); - assertThat(clusterStats.getClusterServiceName()).isEqualTo(EDS_SERVICE_NAME); - assertThat(clusterStats.getTotalDroppedRequests()).isEqualTo(0L); + assertThat(clusterStats.clusterServiceName()).isEqualTo(EDS_SERVICE_NAME); + assertThat(clusterStats.totalDroppedRequests()).isEqualTo(0L); PickResult result = currentPicker.pickSubchannel(mock(PickSubchannelArgs.class)); clusterStats = Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER)); - assertThat(clusterStats.getClusterServiceName()).isEqualTo(EDS_SERVICE_NAME); + assertThat(clusterStats.clusterServiceName()).isEqualTo(EDS_SERVICE_NAME); if (enableCircuitBreaking) { assertThat(result.getStatus().isOk()).isFalse(); assertThat(result.getStatus().getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(result.getStatus().getDescription()) .isEqualTo("Cluster max concurrent requests limit exceeded"); - assertThat(clusterStats.getTotalDroppedRequests()).isEqualTo(1L); + assertThat(clusterStats.totalDroppedRequests()).isEqualTo(1L); } else { assertThat(result.getStatus().isOk()).isTrue(); - assertThat(clusterStats.getTotalDroppedRequests()).isEqualTo(0L); + assertThat(clusterStats.totalDroppedRequests()).isEqualTo(0L); } } diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java index 742fe37dd2..3c2c226cd4 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java @@ -59,10 +59,9 @@ import io.grpc.internal.ServiceConfigUtil.PolicySelection; import io.grpc.xds.ClusterImplLoadBalancerProvider.ClusterImplConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig.DiscoveryMechanism; -import io.grpc.xds.EnvoyProtoData.DropOverload; -import io.grpc.xds.EnvoyProtoData.LbEndpoint; -import io.grpc.xds.EnvoyProtoData.Locality; -import io.grpc.xds.EnvoyProtoData.LocalityLbEndpoints; +import io.grpc.xds.Endpoints.DropOverload; +import io.grpc.xds.Endpoints.LbEndpoint; +import io.grpc.xds.Endpoints.LocalityLbEndpoints; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; @@ -104,11 +103,11 @@ public class ClusterResolverLoadBalancerTest { private static final String EDS_SERVICE_NAME2 = "backend-service-bar.googleapis.com"; private static final String LRS_SERVER_NAME = "lrs.googleapis.com"; private final Locality locality1 = - new Locality("test-region-1", "test-zone-1", "test-subzone-1"); + Locality.create("test-region-1", "test-zone-1", "test-subzone-1"); private final Locality locality2 = - new Locality("test-region-2", "test-zone-2", "test-subzone-2"); + Locality.create("test-region-2", "test-zone-2", "test-subzone-2"); private final Locality locality3 = - new Locality("test-region-3", "test-zone-3", "test-subzone-3"); + Locality.create("test-region-3", "test-zone-3", "test-subzone-3"); private final UpstreamTlsContext tlsContext = CommonTlsContextTestsUtil.buildUpstreamTlsContextFromFilenames( CommonTlsContextTestsUtil.CLIENT_KEY_FILE, @@ -580,7 +579,7 @@ public class ClusterResolverLoadBalancerTest { Collections.singletonList(endpoint3)); assertAddressesEqual(AddressFilter.filter(AddressFilter.filter( childBalancer.addresses, CLUSTER_DNS + "[priority0]"), - new Locality("", "", "").toString()), + Locality.create("", "", "").toString()), Arrays.asList(endpoint1, endpoint2)); } @@ -769,9 +768,9 @@ public class ClusterResolverLoadBalancerTest { List endpoints = new ArrayList<>(); for (EquivalentAddressGroup addr : managedEndpoints.keySet()) { boolean status = managedEndpoints.get(addr); - endpoints.add(new LbEndpoint(addr, 100 /* unused */, status)); + endpoints.add(LbEndpoint.create(addr, 100 /* unused */, status)); } - return new LocalityLbEndpoints(endpoints, localityWeight, priority); + return LocalityLbEndpoints.create(endpoints, localityWeight, priority); } private static EquivalentAddressGroup makeAddress(final String name) { diff --git a/xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java b/xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java index 34a2055d1b..65e801c97d 100644 --- a/xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java +++ b/xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java @@ -19,38 +19,10 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableMap; -import com.google.common.testing.EqualsTester; -import com.google.protobuf.BoolValue; import com.google.protobuf.Struct; -import com.google.protobuf.UInt32Value; import com.google.protobuf.Value; -import com.google.protobuf.util.Durations; -import com.google.re2j.Pattern; -import io.envoyproxy.envoy.config.core.v3.RuntimeFractionalPercent; -import io.envoyproxy.envoy.config.route.v3.QueryParameterMatcher; -import io.envoyproxy.envoy.config.route.v3.RedirectAction; -import io.envoyproxy.envoy.config.route.v3.RouteAction.MaxStreamDuration; -import io.envoyproxy.envoy.config.route.v3.WeightedCluster; -import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher; -import io.envoyproxy.envoy.type.v3.FractionalPercent; -import io.envoyproxy.envoy.type.v3.Int64Range; import io.grpc.xds.EnvoyProtoData.Address; -import io.grpc.xds.EnvoyProtoData.ClusterStats; -import io.grpc.xds.EnvoyProtoData.ClusterStats.DroppedRequests; -import io.grpc.xds.EnvoyProtoData.ClusterWeight; -import io.grpc.xds.EnvoyProtoData.EndpointLoadMetricStats; -import io.grpc.xds.EnvoyProtoData.Locality; import io.grpc.xds.EnvoyProtoData.Node; -import io.grpc.xds.EnvoyProtoData.Route; -import io.grpc.xds.EnvoyProtoData.RouteAction; -import io.grpc.xds.EnvoyProtoData.StructOrError; -import io.grpc.xds.EnvoyProtoData.UpstreamLocalityStats; -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 java.util.concurrent.TimeUnit; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -61,41 +33,6 @@ import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class EnvoyProtoDataTest { - @Test - public void locality_convertToAndFromLocalityProto() { - io.envoyproxy.envoy.config.core.v3.Locality locality = - io.envoyproxy.envoy.config.core.v3.Locality.newBuilder() - .setRegion("test_region") - .setZone("test_zone") - .setSubZone("test_subzone") - .build(); - Locality xdsLocality = Locality.fromEnvoyProtoLocality(locality); - assertThat(xdsLocality.getRegion()).isEqualTo("test_region"); - assertThat(xdsLocality.getZone()).isEqualTo("test_zone"); - assertThat(xdsLocality.getSubZone()).isEqualTo("test_subzone"); - - io.envoyproxy.envoy.api.v2.core.Locality convertedLocality = - xdsLocality.toEnvoyProtoLocalityV2(); - assertThat(convertedLocality.getRegion()).isEqualTo("test_region"); - assertThat(convertedLocality.getZone()).isEqualTo("test_zone"); - assertThat(convertedLocality.getSubZone()).isEqualTo("test_subzone"); - } - - @Test - public void locality_equal() { - new EqualsTester() - .addEqualityGroup( - new Locality("region-a", "zone-a", "subzone-a"), - new Locality("region-a", "zone-a", "subzone-a")) - .addEqualityGroup( - new Locality("region", "zone", "subzone") - ) - .addEqualityGroup( - new Locality("", "", ""), - new Locality("", "", "")) - .testEquals(); - } - @SuppressWarnings("deprecation") @Test public void convertNode() { @@ -108,7 +45,7 @@ public class EnvoyProtoDataTest { "ENVOY_PORT", "TRAFFICDIRECTOR_NETWORK_NAME", "VPC_NETWORK_NAME")) - .setLocality(new Locality("region", "zone", "subzone")) + .setLocality(Locality.create("region", "zone", "subzone")) .addListeningAddresses(new Address("www.foo.com", 8080)) .addListeningAddresses(new Address("www.bar.com", 8088)) .setBuildVersion("v1") @@ -185,448 +122,4 @@ public class EnvoyProtoDataTest { .build(); assertThat(node.toEnvoyProtoNodeV2()).isEqualTo(nodeProtoV2); } - - @Test - public void locality_hash() { - assertThat(new Locality("region", "zone", "subzone").hashCode()) - .isEqualTo(new Locality("region", "zone","subzone").hashCode()); - } - - // TODO(chengyuanzhang): add test for other data types. - - @Test - public void convertRoute() { - io.envoyproxy.envoy.config.route.v3.Route proto1 = - io.envoyproxy.envoy.config.route.v3.Route.newBuilder() - .setName("route-blade") - .setMatch( - io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder() - .setPath("/service/method")) - .setRoute( - io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() - .setCluster("cluster-foo")) - .build(); - StructOrError struct1 = Route.fromEnvoyProtoRoute(proto1); - assertThat(struct1.getErrorDetail()).isNull(); - assertThat(struct1.getStruct()) - .isEqualTo( - new Route( - new RouteMatch(PathMatcher.fromPath("/service/method", false), - Collections.emptyList(), null), - new RouteAction(null, "cluster-foo", null))); - - io.envoyproxy.envoy.config.route.v3.Route unsupportedProto = - io.envoyproxy.envoy.config.route.v3.Route.newBuilder() - .setName("route-blade") - .setMatch(io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder().setPath("")) - .setRedirect(RedirectAction.getDefaultInstance()) - .build(); - StructOrError unsupportedStruct = Route.fromEnvoyProtoRoute(unsupportedProto); - assertThat(unsupportedStruct.getErrorDetail()).isNotNull(); - assertThat(unsupportedStruct.getStruct()).isNull(); - } - - @Test - public void convertRoute_skipWithUnsupportedMatcher() { - io.envoyproxy.envoy.config.route.v3.Route proto = - io.envoyproxy.envoy.config.route.v3.Route.newBuilder() - .setName("ignore me") - .setMatch( - io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder() - .setPath("/service/method") - .addQueryParameters( - io.envoyproxy.envoy.config.route.v3.QueryParameterMatcher - .getDefaultInstance())) - .setRoute( - io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() - .setCluster("cluster-foo")) - .build(); - assertThat(Route.fromEnvoyProtoRoute(proto)).isNull(); - } - - @Test - public void convertRoute_skipWithUnsupportedAction() { - io.envoyproxy.envoy.config.route.v3.Route proto = - io.envoyproxy.envoy.config.route.v3.Route.newBuilder() - .setName("ignore me") - .setMatch( - io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder() - .setPath("/service/method")) - .setRoute( - io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() - .setClusterHeader("some cluster header")) - .build(); - assertThat(Route.fromEnvoyProtoRoute(proto)).isNull(); - } - - @Test - public void convertRouteMatch_pathMatching() { - // path_specifier = prefix - io.envoyproxy.envoy.config.route.v3.RouteMatch proto1 = - io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder().setPrefix("/").build(); - StructOrError struct1 = Route.convertEnvoyProtoRouteMatch(proto1); - assertThat(struct1.getErrorDetail()).isNull(); - assertThat(struct1.getStruct()).isEqualTo( - new RouteMatch( - PathMatcher.fromPrefix("/", false), Collections.emptyList(), null)); - - proto1 = proto1.toBuilder().setCaseSensitive(BoolValue.newBuilder().setValue(true)).build(); - struct1 = Route.convertEnvoyProtoRouteMatch(proto1); - assertThat(struct1.getStruct()).isEqualTo( - new RouteMatch( - PathMatcher.fromPrefix("/", true), Collections.emptyList(), null)); - - // path_specifier = path - io.envoyproxy.envoy.config.route.v3.RouteMatch proto2 = - io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder() - .setPath("/service/method") - .build(); - StructOrError struct2 = Route.convertEnvoyProtoRouteMatch(proto2); - assertThat(struct2.getErrorDetail()).isNull(); - assertThat(struct2.getStruct()).isEqualTo( - new RouteMatch( - PathMatcher.fromPath("/service/method", false), - Collections.emptyList(), null)); - - proto2 = proto2.toBuilder().setCaseSensitive(BoolValue.newBuilder().setValue(true)).build(); - struct2 = Route.convertEnvoyProtoRouteMatch(proto2); - assertThat(struct2.getStruct()).isEqualTo( - new RouteMatch( - PathMatcher.fromPath("/service/method", true), - Collections.emptyList(), null)); - - // path_specifier = safe_regex - io.envoyproxy.envoy.config.route.v3.RouteMatch proto4 = - io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder() - .setSafeRegex(RegexMatcher.newBuilder().setRegex(".")) - .build(); - StructOrError struct4 = Route.convertEnvoyProtoRouteMatch(proto4); - assertThat(struct4.getErrorDetail()).isNull(); - assertThat(struct4.getStruct()).isEqualTo( - new RouteMatch( - PathMatcher.fromRegEx(Pattern.compile(".")), - Collections.emptyList(), null)); - - // query_parameters is set - io.envoyproxy.envoy.config.route.v3.RouteMatch proto6 = - io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder() - .addQueryParameters(QueryParameterMatcher.getDefaultInstance()) - .build(); - StructOrError struct6 = Route.convertEnvoyProtoRouteMatch(proto6); - assertThat(struct6).isNull(); - - // path_specifier unset - io.envoyproxy.envoy.config.route.v3.RouteMatch unsetProto = - io.envoyproxy.envoy.config.route.v3.RouteMatch.getDefaultInstance(); - StructOrError unsetStruct = Route.convertEnvoyProtoRouteMatch(unsetProto); - assertThat(unsetStruct.getErrorDetail()).isNotNull(); - assertThat(unsetStruct.getStruct()).isNull(); - } - - @Test - public void convertRouteMatch_withHeaderMatching() { - io.envoyproxy.envoy.config.route.v3.RouteMatch proto = - io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder() - .setPrefix("") - .addHeaders( - io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() - .setName(":scheme") - .setPrefixMatch("http")) - .addHeaders( - io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() - .setName(":method") - .setExactMatch("PUT")) - .build(); - StructOrError struct = Route.convertEnvoyProtoRouteMatch(proto); - assertThat(struct.getErrorDetail()).isNull(); - assertThat(struct.getStruct()) - .isEqualTo( - new RouteMatch( - PathMatcher.fromPrefix("", false), - Arrays.asList( - new HeaderMatcher(":scheme", null, null, null, null, "http", null, false), - new HeaderMatcher(":method", "PUT", null, null, null, null, null, false)), - null)); - } - - @Test - public void convertRouteMatch_withRuntimeFraction() { - io.envoyproxy.envoy.config.route.v3.RouteMatch proto = - io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder() - .setPrefix("") - .setRuntimeFraction( - RuntimeFractionalPercent.newBuilder() - .setDefaultValue( - FractionalPercent.newBuilder() - .setNumerator(30) - .setDenominator(FractionalPercent.DenominatorType.HUNDRED))) - .build(); - StructOrError struct = Route.convertEnvoyProtoRouteMatch(proto); - assertThat(struct.getErrorDetail()).isNull(); - assertThat(struct.getStruct()) - .isEqualTo( - new RouteMatch( - PathMatcher.fromPrefix( "", false), Collections.emptyList(), - new FractionMatcher(30, 100))); - } - - @Test - public void convertRouteAction_cluster() { - io.envoyproxy.envoy.config.route.v3.RouteAction proto = - io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() - .setCluster("cluster-foo") - .build(); - StructOrError struct = RouteAction.fromEnvoyProtoRouteAction(proto); - assertThat(struct.getErrorDetail()).isNull(); - assertThat(struct.getStruct().getCluster()).isEqualTo("cluster-foo"); - assertThat(struct.getStruct().getWeightedCluster()).isNull(); - } - - @Test - public void convertRouteAction_weightedCluster() { - io.envoyproxy.envoy.config.route.v3.RouteAction proto = - io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() - .setWeightedClusters( - WeightedCluster.newBuilder() - .addClusters( - WeightedCluster.ClusterWeight - .newBuilder() - .setName("cluster-foo") - .setWeight(UInt32Value.newBuilder().setValue(30))) - .addClusters(WeightedCluster.ClusterWeight - .newBuilder() - .setName("cluster-bar") - .setWeight(UInt32Value.newBuilder().setValue(70)))) - .build(); - StructOrError struct = RouteAction.fromEnvoyProtoRouteAction(proto); - assertThat(struct.getErrorDetail()).isNull(); - assertThat(struct.getStruct().getCluster()).isNull(); - assertThat(struct.getStruct().getWeightedCluster()).containsExactly( - new ClusterWeight("cluster-foo", 30, null), new ClusterWeight("cluster-bar", 70, null)); - } - - @Test - public void convertRouteAction_unspecifiedClusterError() { - io.envoyproxy.envoy.config.route.v3.RouteAction proto = - io.envoyproxy.envoy.config.route.v3.RouteAction.getDefaultInstance(); - StructOrError unsetStruct = RouteAction.fromEnvoyProtoRouteAction(proto); - assertThat(unsetStruct.getStruct()).isNull(); - assertThat(unsetStruct.getErrorDetail()).isNotNull(); - } - - @Test - public void convertRouteAction_timeoutByGrpcTimeoutHeaderMax() { - io.envoyproxy.envoy.config.route.v3.RouteAction proto = - io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() - .setCluster("cluster-foo") - .setMaxStreamDuration( - MaxStreamDuration.newBuilder() - .setGrpcTimeoutHeaderMax(Durations.fromSeconds(5L)) - .setMaxStreamDuration(Durations.fromMillis(20L))) - .build(); - StructOrError struct = RouteAction.fromEnvoyProtoRouteAction(proto); - assertThat(struct.getStruct().getTimeoutNano()).isEqualTo(TimeUnit.SECONDS.toNanos(5L)); - } - - @Test - public void convertRouteAction_timeoutByMaxStreamDuration() { - io.envoyproxy.envoy.config.route.v3.RouteAction proto = - io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() - .setCluster("cluster-foo") - .setMaxStreamDuration( - MaxStreamDuration.newBuilder() - .setMaxStreamDuration(Durations.fromSeconds(5L))) - .build(); - StructOrError struct = RouteAction.fromEnvoyProtoRouteAction(proto); - assertThat(struct.getStruct().getTimeoutNano()).isEqualTo(TimeUnit.SECONDS.toNanos(5L)); - } - - @Test - public void convertRouteAction_timeoutUnset() { - io.envoyproxy.envoy.config.route.v3.RouteAction proto = - io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() - .setCluster("cluster-foo") - .build(); - StructOrError struct = RouteAction.fromEnvoyProtoRouteAction(proto); - assertThat(struct.getStruct().getTimeoutNano()).isNull(); - } - - @Test - public void convertHeaderMatcher() { - // header_match_specifier = exact_match - io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto1 = - io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() - .setName(":method") - .setExactMatch("PUT") - .build(); - StructOrError struct1 = Route.convertEnvoyProtoHeaderMatcher(proto1); - assertThat(struct1.getErrorDetail()).isNull(); - assertThat(struct1.getStruct()).isEqualTo( - new HeaderMatcher(":method", "PUT", null, null, null, null, null, false)); - - // header_match_specifier = safe_regex_match - io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto3 = - io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() - .setName(":method") - .setSafeRegexMatch(RegexMatcher.newBuilder().setRegex("P*")) - .build(); - 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)); - - // header_match_specifier = range_match - io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto4 = - io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() - .setName("timeout") - .setRangeMatch(Int64Range.newBuilder().setStart(10L).setEnd(20L)) - .build(); - StructOrError struct4 = Route.convertEnvoyProtoHeaderMatcher(proto4); - assertThat(struct4.getErrorDetail()).isNull(); - assertThat(struct4.getStruct()).isEqualTo( - new HeaderMatcher( - "timeout", null, null, new HeaderMatcher.Range(10L, 20L), null, null, null, false)); - - // header_match_specifier = present_match - io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto5 = - io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() - .setName("user-agent") - .setPresentMatch(true) - .build(); - StructOrError struct5 = Route.convertEnvoyProtoHeaderMatcher(proto5); - assertThat(struct5.getErrorDetail()).isNull(); - assertThat(struct5.getStruct()).isEqualTo( - new HeaderMatcher("user-agent", null, null, null, true, null, null, false)); - - // header_match_specifier = prefix_match - io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto6 = - io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() - .setName("authority") - .setPrefixMatch("service-foo") - .build(); - StructOrError struct6 = Route.convertEnvoyProtoHeaderMatcher(proto6); - assertThat(struct6.getErrorDetail()).isNull(); - assertThat(struct6.getStruct()).isEqualTo( - new HeaderMatcher("authority", null, null, null, null, "service-foo", null, false)); - - // header_match_specifier = suffix_match - io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto7 = - io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() - .setName("authority") - .setSuffixMatch("googleapis.com") - .build(); - StructOrError struct7 = Route.convertEnvoyProtoHeaderMatcher(proto7); - assertThat(struct7.getErrorDetail()).isNull(); - assertThat(struct7.getStruct()).isEqualTo( - new HeaderMatcher( - "authority", null, null, null, null, null, "googleapis.com", false)); - - // header_match_specifier unset - io.envoyproxy.envoy.config.route.v3.HeaderMatcher unsetProto = - io.envoyproxy.envoy.config.route.v3.HeaderMatcher.getDefaultInstance(); - StructOrError unsetStruct = Route.convertEnvoyProtoHeaderMatcher(unsetProto); - assertThat(unsetStruct.getErrorDetail()).isNotNull(); - assertThat(unsetStruct.getStruct()).isNull(); - } - - @Test - public void convertHeaderMatcher_malformedRegExPattern() { - io.envoyproxy.envoy.config.route.v3.HeaderMatcher proto = - io.envoyproxy.envoy.config.route.v3.HeaderMatcher.newBuilder() - .setName(":method") - .setSafeRegexMatch(RegexMatcher.newBuilder().setRegex("[")) - .build(); - StructOrError struct = Route.convertEnvoyProtoHeaderMatcher(proto); - assertThat(struct.getErrorDetail()).isNotNull(); - assertThat(struct.getStruct()).isNull(); - } - - @Test - public void convertClusterWeight() { - io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight proto = - io.envoyproxy.envoy.config.route.v3.WeightedCluster.ClusterWeight.newBuilder() - .setName("cluster-foo") - .setWeight(UInt32Value.newBuilder().setValue(30)).build(); - ClusterWeight struct = ClusterWeight.fromEnvoyProtoClusterWeight(proto).getStruct(); - assertThat(struct.getName()).isEqualTo("cluster-foo"); - assertThat(struct.getWeight()).isEqualTo(30); - } - - @Test - public void clusterStats_convertToEnvoyProto() { - ClusterStats clusterStats = - ClusterStats.newBuilder() - .setClusterName("cluster1") - .setClusterServiceName("backend-service1") - .setLoadReportIntervalNanos(1234) - .setTotalDroppedRequests(123) - .addUpstreamLocalityStats(UpstreamLocalityStats.newBuilder() - .setLocality(new Locality("region1", "zone1", "subzone1")) - .setTotalErrorRequests(1) - .setTotalRequestsInProgress(2) - .setTotalSuccessfulRequests(100) - .setTotalIssuedRequests(103) - .addLoadMetricStats(EndpointLoadMetricStats.newBuilder() - .setMetricName("metric1") - .setNumRequestsFinishedWithMetric(1000) - .setTotalMetricValue(0.5D) - .build()) - .build()) - .addDroppedRequests(new DroppedRequests("category1", 100)) - .build(); - - io.envoyproxy.envoy.config.endpoint.v3.ClusterStats clusterStatsProto = - clusterStats.toEnvoyProtoClusterStats(); - assertThat(clusterStatsProto).isEqualTo( - io.envoyproxy.envoy.config.endpoint.v3.ClusterStats.newBuilder() - .setClusterName("cluster1") - .setClusterServiceName("backend-service1") - .setLoadReportInterval(Durations.fromNanos(1234)) - .setTotalDroppedRequests(123) - .addUpstreamLocalityStats( - io.envoyproxy.envoy.config.endpoint.v3.UpstreamLocalityStats.newBuilder() - .setLocality( - new Locality("region1", "zone1", "subzone1").toEnvoyProtoLocality()) - .setTotalErrorRequests(1) - .setTotalRequestsInProgress(2) - .setTotalSuccessfulRequests(100) - .setTotalIssuedRequests(103) - .addLoadMetricStats( - io.envoyproxy.envoy.config.endpoint.v3.EndpointLoadMetricStats.newBuilder() - .setMetricName("metric1") - .setNumRequestsFinishedWithMetric(1000) - .setTotalMetricValue(0.5D))) - .addDroppedRequests( - io.envoyproxy.envoy.config.endpoint.v3.ClusterStats.DroppedRequests.newBuilder() - .setCategory("category1") - .setDroppedCount(100)) - .build()); - - io.envoyproxy.envoy.api.v2.endpoint.ClusterStats clusterStatsProtoV2 = - clusterStats.toEnvoyProtoClusterStatsV2(); - assertThat(clusterStatsProtoV2).isEqualTo( - io.envoyproxy.envoy.api.v2.endpoint.ClusterStats.newBuilder() - .setClusterName("cluster1") - .setClusterServiceName("backend-service1") - .setLoadReportInterval(Durations.fromNanos(1234)) - .setTotalDroppedRequests(123) - .addUpstreamLocalityStats( - io.envoyproxy.envoy.api.v2.endpoint.UpstreamLocalityStats.newBuilder() - .setLocality( - new Locality("region1", "zone1", "subzone1").toEnvoyProtoLocalityV2()) - .setTotalErrorRequests(1) - .setTotalRequestsInProgress(2) - .setTotalSuccessfulRequests(100) - .setTotalIssuedRequests(103) - .addLoadMetricStats( - io.envoyproxy.envoy.api.v2.endpoint.EndpointLoadMetricStats.newBuilder() - .setMetricName("metric1") - .setNumRequestsFinishedWithMetric(1000) - .setTotalMetricValue(0.5D))) - .addDroppedRequests( - io.envoyproxy.envoy.api.v2.endpoint.ClusterStats.DroppedRequests.newBuilder() - .setCategory("category1") - .setDroppedCount(100)) - .build()); - } } diff --git a/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java b/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java index 0436065344..cc46d57202 100644 --- a/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java +++ b/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java @@ -50,7 +50,6 @@ import io.grpc.internal.BackoffPolicy; import io.grpc.internal.FakeClock; import io.grpc.stub.StreamObserver; import io.grpc.testing.GrpcCleanupRule; -import io.grpc.xds.EnvoyProtoData.Locality; import io.grpc.xds.LoadStatsManager2.ClusterDropStats; import io.grpc.xds.LoadStatsManager2.ClusterLocalityStats; import java.util.ArrayDeque; @@ -85,8 +84,8 @@ public class LoadReportClientTest { private static final String CLUSTER2 = "cluster-bar.googleapis.com"; private static final String EDS_SERVICE_NAME1 = "backend-service-foo.googleapis.com"; private static final String EDS_SERVICE_NAME2 = "backend-service-bar.googleapis.com"; - private static final Locality LOCALITY1 = new Locality("region1", "zone1", "subZone1"); - private static final Locality LOCALITY2 = new Locality("region2", "zone2", "subZone2"); + private static final Locality LOCALITY1 = Locality.create("region1", "zone1", "subZone1"); + private static final Locality LOCALITY2 = Locality.create("region2", "zone2", "subZone2"); private static final FakeClock.TaskFilter LOAD_REPORTING_TASK_FILTER = new FakeClock.TaskFilter() { @Override diff --git a/xds/src/test/java/io/grpc/xds/LoadStatsManager2Test.java b/xds/src/test/java/io/grpc/xds/LoadStatsManager2Test.java index afdfee4da1..0cfb7f46a2 100644 --- a/xds/src/test/java/io/grpc/xds/LoadStatsManager2Test.java +++ b/xds/src/test/java/io/grpc/xds/LoadStatsManager2Test.java @@ -21,12 +21,11 @@ import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.Iterables; import io.grpc.Status; import io.grpc.internal.FakeClock; -import io.grpc.xds.EnvoyProtoData.ClusterStats; -import io.grpc.xds.EnvoyProtoData.ClusterStats.DroppedRequests; -import io.grpc.xds.EnvoyProtoData.Locality; -import io.grpc.xds.EnvoyProtoData.UpstreamLocalityStats; import io.grpc.xds.LoadStatsManager2.ClusterDropStats; import io.grpc.xds.LoadStatsManager2.ClusterLocalityStats; +import io.grpc.xds.Stats.ClusterStats; +import io.grpc.xds.Stats.DroppedRequests; +import io.grpc.xds.Stats.UpstreamLocalityStats; import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -45,11 +44,11 @@ public class LoadStatsManager2Test { private static final String EDS_SERVICE_NAME1 = "backend-service-foo.googleapis.com"; private static final String EDS_SERVICE_NAME2 = "backend-service-bar.googleapis.com"; private static final Locality LOCALITY1 = - new Locality("test_region1", "test_zone1", "test_subzone1"); + Locality.create("test_region1", "test_zone1", "test_subzone1"); private static final Locality LOCALITY2 = - new Locality("test_region2", "test_zone2", "test_subzone2"); + Locality.create("test_region2", "test_zone2", "test_subzone2"); private static final Locality LOCALITY3 = - new Locality("test_region3", "test_zone3", "test_subzone3"); + Locality.create("test_region3", "test_zone3", "test_subzone3"); private final FakeClock fakeClock = new FakeClock(); private final LoadStatsManager2 loadStatsManager = @@ -85,68 +84,68 @@ public class LoadStatsManager2Test { assertThat(allStats).hasSize(3); // three cluster:edsServiceName ClusterStats stats1 = findClusterStats(allStats, CLUSTER_NAME1, EDS_SERVICE_NAME1); - assertThat(stats1.getLoadReportIntervalNanos()).isEqualTo(TimeUnit.SECONDS.toNanos(5L + 10L)); - assertThat(stats1.getDroppedRequestsList()).hasSize(2); - assertThat(findDroppedRequestCount(stats1.getDroppedRequestsList(), "lb")).isEqualTo(1L); - assertThat(findDroppedRequestCount(stats1.getDroppedRequestsList(), "throttle")).isEqualTo(1L); - assertThat(stats1.getTotalDroppedRequests()).isEqualTo(1L + 1L); - assertThat(stats1.getUpstreamLocalityStatsList()).hasSize(2); // two localities + assertThat(stats1.loadReportIntervalNano()).isEqualTo(TimeUnit.SECONDS.toNanos(5L + 10L)); + assertThat(stats1.droppedRequestsList()).hasSize(2); + assertThat(findDroppedRequestCount(stats1.droppedRequestsList(), "lb")).isEqualTo(1L); + assertThat(findDroppedRequestCount(stats1.droppedRequestsList(), "throttle")).isEqualTo(1L); + assertThat(stats1.totalDroppedRequests()).isEqualTo(1L + 1L); + assertThat(stats1.upstreamLocalityStatsList()).hasSize(2); // two localities UpstreamLocalityStats loadStats1 = - findLocalityStats(stats1.getUpstreamLocalityStatsList(), LOCALITY1); - assertThat(loadStats1.getTotalIssuedRequests()).isEqualTo(19L); - assertThat(loadStats1.getTotalSuccessfulRequests()).isEqualTo(1L); - assertThat(loadStats1.getTotalErrorRequests()).isEqualTo(0L); - assertThat(loadStats1.getTotalRequestsInProgress()).isEqualTo(19L - 1L); + findLocalityStats(stats1.upstreamLocalityStatsList(), LOCALITY1); + assertThat(loadStats1.totalIssuedRequests()).isEqualTo(19L); + assertThat(loadStats1.totalSuccessfulRequests()).isEqualTo(1L); + assertThat(loadStats1.totalErrorRequests()).isEqualTo(0L); + assertThat(loadStats1.totalRequestsInProgress()).isEqualTo(19L - 1L); UpstreamLocalityStats loadStats2 = - findLocalityStats(stats1.getUpstreamLocalityStatsList(), LOCALITY2); - assertThat(loadStats2.getTotalIssuedRequests()).isEqualTo(9L); - assertThat(loadStats2.getTotalSuccessfulRequests()).isEqualTo(0L); - assertThat(loadStats2.getTotalErrorRequests()).isEqualTo(1L); - assertThat(loadStats2.getTotalRequestsInProgress()).isEqualTo(9L - 1L); + findLocalityStats(stats1.upstreamLocalityStatsList(), LOCALITY2); + assertThat(loadStats2.totalIssuedRequests()).isEqualTo(9L); + assertThat(loadStats2.totalSuccessfulRequests()).isEqualTo(0L); + assertThat(loadStats2.totalErrorRequests()).isEqualTo(1L); + assertThat(loadStats2.totalRequestsInProgress()).isEqualTo(9L - 1L); ClusterStats stats2 = findClusterStats(allStats, CLUSTER_NAME1, EDS_SERVICE_NAME2); - assertThat(stats2.getLoadReportIntervalNanos()).isEqualTo(TimeUnit.SECONDS.toNanos(5L + 10L)); - assertThat(stats2.getDroppedRequestsList()).isEmpty(); // no categorized drops - assertThat(stats2.getTotalDroppedRequests()).isEqualTo(1L); - assertThat(stats2.getUpstreamLocalityStatsList()).isEmpty(); // no per-locality stats + assertThat(stats2.loadReportIntervalNano()).isEqualTo(TimeUnit.SECONDS.toNanos(5L + 10L)); + assertThat(stats2.droppedRequestsList()).isEmpty(); // no categorized drops + assertThat(stats2.totalDroppedRequests()).isEqualTo(1L); + assertThat(stats2.upstreamLocalityStatsList()).isEmpty(); // no per-locality stats ClusterStats stats3 = findClusterStats(allStats, CLUSTER_NAME2, null); - assertThat(stats3.getLoadReportIntervalNanos()).isEqualTo(TimeUnit.SECONDS.toNanos(5L + 10L)); - assertThat(stats3.getDroppedRequestsList()).isEmpty(); - assertThat(stats3.getTotalDroppedRequests()).isEqualTo(0L); // no drops recorded - assertThat(stats3.getUpstreamLocalityStatsList()).hasSize(1); // one localities + assertThat(stats3.loadReportIntervalNano()).isEqualTo(TimeUnit.SECONDS.toNanos(5L + 10L)); + assertThat(stats3.droppedRequestsList()).isEmpty(); + assertThat(stats3.totalDroppedRequests()).isEqualTo(0L); // no drops recorded + assertThat(stats3.upstreamLocalityStatsList()).hasSize(1); // one localities UpstreamLocalityStats loadStats3 = - Iterables.getOnlyElement(stats3.getUpstreamLocalityStatsList()); - assertThat(loadStats3.getTotalIssuedRequests()).isEqualTo(1L); - assertThat(loadStats3.getTotalSuccessfulRequests()).isEqualTo(0L); - assertThat(loadStats3.getTotalErrorRequests()).isEqualTo(0L); - assertThat(loadStats3.getTotalRequestsInProgress()).isEqualTo(1L); + Iterables.getOnlyElement(stats3.upstreamLocalityStatsList()); + assertThat(loadStats3.totalIssuedRequests()).isEqualTo(1L); + assertThat(loadStats3.totalSuccessfulRequests()).isEqualTo(0L); + assertThat(loadStats3.totalErrorRequests()).isEqualTo(0L); + assertThat(loadStats3.totalRequestsInProgress()).isEqualTo(1L); fakeClock.forwardTime(3L, TimeUnit.SECONDS); List clusterStatsList = loadStatsManager.getClusterStatsReports(CLUSTER_NAME1); assertThat(clusterStatsList).hasSize(2); stats1 = findClusterStats(clusterStatsList, CLUSTER_NAME1, EDS_SERVICE_NAME1); - assertThat(stats1.getLoadReportIntervalNanos()).isEqualTo(TimeUnit.SECONDS.toNanos(3L)); - assertThat(stats1.getDroppedRequestsList()).isEmpty(); - assertThat(stats1.getTotalDroppedRequests()).isEqualTo(0L); // no new drops recorded - assertThat(stats1.getUpstreamLocalityStatsList()).hasSize(2); // two localities - loadStats1 = findLocalityStats(stats1.getUpstreamLocalityStatsList(), LOCALITY1); - assertThat(loadStats1.getTotalIssuedRequests()).isEqualTo(0L); - assertThat(loadStats1.getTotalSuccessfulRequests()).isEqualTo(0L); - assertThat(loadStats1.getTotalErrorRequests()).isEqualTo(0L); - assertThat(loadStats1.getTotalRequestsInProgress()).isEqualTo(18L); // still in-progress - loadStats2 = findLocalityStats(stats1.getUpstreamLocalityStatsList(), LOCALITY2); - assertThat(loadStats2.getTotalIssuedRequests()).isEqualTo(0L); - assertThat(loadStats2.getTotalSuccessfulRequests()).isEqualTo(0L); - assertThat(loadStats2.getTotalErrorRequests()).isEqualTo(0L); - assertThat(loadStats2.getTotalRequestsInProgress()).isEqualTo(8L); // still in-progress + assertThat(stats1.loadReportIntervalNano()).isEqualTo(TimeUnit.SECONDS.toNanos(3L)); + assertThat(stats1.droppedRequestsList()).isEmpty(); + assertThat(stats1.totalDroppedRequests()).isEqualTo(0L); // no new drops recorded + assertThat(stats1.upstreamLocalityStatsList()).hasSize(2); // two localities + loadStats1 = findLocalityStats(stats1.upstreamLocalityStatsList(), LOCALITY1); + assertThat(loadStats1.totalIssuedRequests()).isEqualTo(0L); + assertThat(loadStats1.totalSuccessfulRequests()).isEqualTo(0L); + assertThat(loadStats1.totalErrorRequests()).isEqualTo(0L); + assertThat(loadStats1.totalRequestsInProgress()).isEqualTo(18L); // still in-progress + loadStats2 = findLocalityStats(stats1.upstreamLocalityStatsList(), LOCALITY2); + assertThat(loadStats2.totalIssuedRequests()).isEqualTo(0L); + assertThat(loadStats2.totalSuccessfulRequests()).isEqualTo(0L); + assertThat(loadStats2.totalErrorRequests()).isEqualTo(0L); + assertThat(loadStats2.totalRequestsInProgress()).isEqualTo(8L); // still in-progress stats2 = findClusterStats(clusterStatsList, CLUSTER_NAME1, EDS_SERVICE_NAME2); - assertThat(stats2.getLoadReportIntervalNanos()).isEqualTo(TimeUnit.SECONDS.toNanos(3L)); - assertThat(stats2.getDroppedRequestsList()).isEmpty(); - assertThat(stats2.getTotalDroppedRequests()).isEqualTo(0L); // no new drops recorded - assertThat(stats2.getUpstreamLocalityStatsList()).isEmpty(); // no per-locality stats + assertThat(stats2.loadReportIntervalNano()).isEqualTo(TimeUnit.SECONDS.toNanos(3L)); + assertThat(stats2.droppedRequestsList()).isEmpty(); + assertThat(stats2.totalDroppedRequests()).isEqualTo(0L); // no new drops recorded + assertThat(stats2.upstreamLocalityStatsList()).isEmpty(); // no per-locality stats } @Test @@ -162,10 +161,10 @@ public class LoadStatsManager2Test { ClusterStats stats = Iterables.getOnlyElement( loadStatsManager.getClusterStatsReports(CLUSTER_NAME1)); - assertThat(stats.getDroppedRequestsList()).hasSize(2); - assertThat(findDroppedRequestCount(stats.getDroppedRequestsList(), "lb")).isEqualTo(1L); - assertThat(findDroppedRequestCount(stats.getDroppedRequestsList(), "throttle")).isEqualTo(1L); - assertThat(stats.getTotalDroppedRequests()).isEqualTo(4L); // 2 cagetorized + 2 uncategoized + assertThat(stats.droppedRequestsList()).hasSize(2); + assertThat(findDroppedRequestCount(stats.droppedRequestsList(), "lb")).isEqualTo(1L); + assertThat(findDroppedRequestCount(stats.droppedRequestsList(), "throttle")).isEqualTo(1L); + assertThat(stats.totalDroppedRequests()).isEqualTo(4L); // 2 cagetorized + 2 uncategoized } @Test @@ -175,15 +174,15 @@ public class LoadStatsManager2Test { counter.recordDroppedRequest("lb"); ClusterStats stats = Iterables.getOnlyElement( loadStatsManager.getClusterStatsReports(CLUSTER_NAME1)); - assertThat(stats.getDroppedRequestsList()).hasSize(1); - assertThat(Iterables.getOnlyElement(stats.getDroppedRequestsList()).getDroppedCount()) + assertThat(stats.droppedRequestsList()).hasSize(1); + assertThat(Iterables.getOnlyElement(stats.droppedRequestsList()).droppedCount()) .isEqualTo(1L); - assertThat(stats.getTotalDroppedRequests()).isEqualTo(1L); + assertThat(stats.totalDroppedRequests()).isEqualTo(1L); counter.release(); stats = Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER_NAME1)); - assertThat(stats.getDroppedRequestsList()).isEmpty(); - assertThat(stats.getTotalDroppedRequests()).isEqualTo(0L); + assertThat(stats.droppedRequestsList()).isEmpty(); + assertThat(stats.totalDroppedRequests()).isEqualTo(0L); assertThat(loadStatsManager.getClusterStatsReports(CLUSTER_NAME1)).isEmpty(); } @@ -203,11 +202,11 @@ public class LoadStatsManager2Test { ClusterStats stats = Iterables.getOnlyElement( loadStatsManager.getClusterStatsReports(CLUSTER_NAME1)); UpstreamLocalityStats localityStats = - Iterables.getOnlyElement(stats.getUpstreamLocalityStatsList()); - assertThat(localityStats.getTotalIssuedRequests()).isEqualTo(1L + 2L); - assertThat(localityStats.getTotalSuccessfulRequests()).isEqualTo(1L); - assertThat(localityStats.getTotalErrorRequests()).isEqualTo(1L); - assertThat(localityStats.getTotalRequestsInProgress()).isEqualTo(1L + 2L - 1L - 1L); + Iterables.getOnlyElement(stats.upstreamLocalityStatsList()); + assertThat(localityStats.totalIssuedRequests()).isEqualTo(1L + 2L); + assertThat(localityStats.totalSuccessfulRequests()).isEqualTo(1L); + assertThat(localityStats.totalErrorRequests()).isEqualTo(1L); + assertThat(localityStats.totalRequestsInProgress()).isEqualTo(1L + 2L - 1L - 1L); } @Test @@ -220,30 +219,30 @@ public class LoadStatsManager2Test { ClusterStats stats = Iterables.getOnlyElement( loadStatsManager.getClusterStatsReports(CLUSTER_NAME1)); UpstreamLocalityStats localityStats = - Iterables.getOnlyElement(stats.getUpstreamLocalityStatsList()); - assertThat(localityStats.getTotalIssuedRequests()).isEqualTo(2L); - assertThat(localityStats.getTotalSuccessfulRequests()).isEqualTo(0L); - assertThat(localityStats.getTotalErrorRequests()).isEqualTo(0L); - assertThat(localityStats.getTotalRequestsInProgress()).isEqualTo(2L); + Iterables.getOnlyElement(stats.upstreamLocalityStatsList()); + assertThat(localityStats.totalIssuedRequests()).isEqualTo(2L); + assertThat(localityStats.totalSuccessfulRequests()).isEqualTo(0L); + assertThat(localityStats.totalErrorRequests()).isEqualTo(0L); + assertThat(localityStats.totalRequestsInProgress()).isEqualTo(2L); // release the counter, but requests still in-flight counter.release(); stats = Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER_NAME1)); - localityStats = Iterables.getOnlyElement(stats.getUpstreamLocalityStatsList()); - assertThat(localityStats.getTotalIssuedRequests()).isEqualTo(0L); - assertThat(localityStats.getTotalSuccessfulRequests()).isEqualTo(0L); - assertThat(localityStats.getTotalErrorRequests()).isEqualTo(0L); - assertThat(localityStats.getTotalRequestsInProgress()) + localityStats = Iterables.getOnlyElement(stats.upstreamLocalityStatsList()); + assertThat(localityStats.totalIssuedRequests()).isEqualTo(0L); + assertThat(localityStats.totalSuccessfulRequests()).isEqualTo(0L); + assertThat(localityStats.totalErrorRequests()).isEqualTo(0L); + assertThat(localityStats.totalRequestsInProgress()) .isEqualTo(2L); // retained by in-flight calls counter.recordCallFinished(Status.OK); counter.recordCallFinished(Status.UNAVAILABLE); stats = Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER_NAME1)); - localityStats = Iterables.getOnlyElement(stats.getUpstreamLocalityStatsList()); - assertThat(localityStats.getTotalIssuedRequests()).isEqualTo(0L); - assertThat(localityStats.getTotalSuccessfulRequests()).isEqualTo(1L); - assertThat(localityStats.getTotalErrorRequests()).isEqualTo(1L); - assertThat(localityStats.getTotalRequestsInProgress()).isEqualTo(0L); + localityStats = Iterables.getOnlyElement(stats.upstreamLocalityStatsList()); + assertThat(localityStats.totalIssuedRequests()).isEqualTo(0L); + assertThat(localityStats.totalSuccessfulRequests()).isEqualTo(1L); + assertThat(localityStats.totalErrorRequests()).isEqualTo(1L); + assertThat(localityStats.totalRequestsInProgress()).isEqualTo(0L); assertThat(loadStatsManager.getClusterStatsReports(CLUSTER_NAME1)).isEmpty(); } @@ -252,8 +251,8 @@ public class LoadStatsManager2Test { private static ClusterStats findClusterStats( List statsList, String cluster, @Nullable String edsServiceName) { for (ClusterStats stats : statsList) { - if (stats.getClusterName().equals(cluster) - && Objects.equals(stats.getClusterServiceName(), edsServiceName)) { + if (stats.clusterName().equals(cluster) + && Objects.equals(stats.clusterServiceName(), edsServiceName)) { return stats; } } @@ -264,7 +263,7 @@ public class LoadStatsManager2Test { private static UpstreamLocalityStats findLocalityStats( List localityStatsList, Locality locality) { for (UpstreamLocalityStats stats : localityStatsList) { - if (stats.getLocality().equals(locality)) { + if (stats.locality().equals(locality)) { return stats; } } @@ -275,11 +274,11 @@ public class LoadStatsManager2Test { List droppedRequestsLists, String category) { DroppedRequests drop = null; for (DroppedRequests stats : droppedRequestsLists) { - if (stats.getCategory().equals(category)) { + if (stats.category().equals(category)) { drop = stats; } } assertThat(drop).isNotNull(); - return drop.getDroppedCount(); + return drop.droppedCount(); } } diff --git a/xds/src/test/java/io/grpc/xds/RouteMatchTest.java b/xds/src/test/java/io/grpc/xds/RouteMatchTest.java deleted file mode 100644 index 1054354701..0000000000 --- a/xds/src/test/java/io/grpc/xds/RouteMatchTest.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * 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 com.google.re2j.Pattern; -import io.grpc.xds.RouteMatch.FractionMatcher; -import io.grpc.xds.RouteMatch.HeaderMatcher; -import io.grpc.xds.RouteMatch.HeaderMatcher.Range; -import io.grpc.xds.RouteMatch.PathMatcher; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Tests for {@link RouteMatch}. */ -@RunWith(JUnit4.class) -public class RouteMatchTest { - - private final Map> headers = new HashMap<>(); - - @Before - public void setUp() { - headers.put("authority", Collections.singletonList("foo.googleapis.com")); - headers.put("grpc-encoding", Collections.singletonList("gzip")); - headers.put("user-agent", Collections.singletonList("gRPC-Java")); - headers.put("content-length", Collections.singletonList("1000")); - headers.put("custom-key", Arrays.asList("custom-value1", "custom-value2")); - } - - @Test - public void routeMatching_pathOnly() { - RouteMatch routeMatch1 = - new RouteMatch( - PathMatcher.fromPath("/FooService/barMethod", true), - Collections.emptyList(), null); - assertThat(routeMatch1.matches("/FooService/barMethod", headers)).isTrue(); - assertThat(routeMatch1.matches("/FooService/bazMethod", headers)).isFalse(); - - RouteMatch routeMatch2 = - new RouteMatch( - PathMatcher.fromPrefix("/FooService/", true), - Collections.emptyList(), null); - assertThat(routeMatch2.matches("/FooService/barMethod", headers)).isTrue(); - assertThat(routeMatch2.matches("/FooService/bazMethod", headers)).isTrue(); - assertThat(routeMatch2.matches("/BarService/bazMethod", headers)).isFalse(); - - RouteMatch routeMatch3 = - new RouteMatch( - PathMatcher.fromRegEx(Pattern.compile(".*Foo.*")), - Collections.emptyList(), null); - assertThat(routeMatch3.matches("/FooService/barMethod", headers)).isTrue(); - } - - @Test - public void pathMatching_caseInsensitive() { - PathMatcher pathMatcher1 = PathMatcher.fromPath("/FooService/barMethod", false); - assertThat(pathMatcher1.matches("/fooservice/barmethod")).isTrue(); - - PathMatcher pathMatcher2 = PathMatcher.fromPrefix("/FooService", false); - assertThat(pathMatcher2.matches("/fooservice/barmethod")).isTrue(); - } - - @Test - public void routeMatching_withHeaders() { - PathMatcher pathMatcher = PathMatcher.fromPath("/FooService/barMethod", true); - RouteMatch routeMatch1 = new RouteMatch( - pathMatcher, - Arrays.asList( - new HeaderMatcher( - "grpc-encoding", "gzip", null, null, null, null, null, false), - new HeaderMatcher( - "authority", null, Pattern.compile(".*googleapis.*"), null, null, null, - null, false), - new HeaderMatcher( - "content-length", null, null, new Range(100, 10000), null, null, null, false), - new HeaderMatcher("user-agent", null, null, null, true, null, null, false), - new HeaderMatcher("custom-key", null, null, null, null, "custom-", null, false), - new HeaderMatcher("custom-key", null, null, null, null, null, "value2", false)), - null); - assertThat(routeMatch1.matches("/FooService/barMethod", headers)).isTrue(); - - RouteMatch routeMatch2 = new RouteMatch( - pathMatcher, - Collections.singletonList( - new HeaderMatcher( - "authority", null, Pattern.compile(".*googleapis.*"), null, null, null, - null, true)), - null); - assertThat(routeMatch2.matches("/FooService/barMethod", headers)).isFalse(); - - RouteMatch routeMatch3 = new RouteMatch( - pathMatcher, - Collections.singletonList( - new HeaderMatcher( - "user-agent", "gRPC-Go", null, null, null, null, - null, false)), - null); - assertThat(routeMatch3.matches("/FooService/barMethod", headers)).isFalse(); - - RouteMatch routeMatch4 = new RouteMatch( - pathMatcher, - Collections.singletonList( - new HeaderMatcher( - "user-agent", null, null, null, false, null, - null, false)), - null); - assertThat(routeMatch4.matches("/FooService/barMethod", headers)).isFalse(); - - RouteMatch routeMatch5 = new RouteMatch( - pathMatcher, - Collections.singletonList( - new HeaderMatcher( - "user-agent", null, null, null, false, null, - null, true)), - null); - assertThat(routeMatch5.matches("/FooService/barMethod", headers)).isTrue(); - - RouteMatch routeMatch6 = new RouteMatch( - pathMatcher, - Collections.singletonList( - new HeaderMatcher( - "user-agent", null, null, null, true, null, - null, true)), - null); - assertThat(routeMatch6.matches("/FooService/barMethod", headers)).isFalse(); - - RouteMatch routeMatch7 = new RouteMatch( - pathMatcher, - Collections.singletonList( - new HeaderMatcher( - "custom-key", "custom-value1,custom-value2", null, null, null, null, - null, false)), - null); - assertThat(routeMatch7.matches("/FooService/barMethod", headers)).isTrue(); - } - - @Test - public void routeMatching_withRuntimeFraction() { - PathMatcher pathMatcher = PathMatcher.fromPath("/FooService/barMethod", true); - RouteMatch routeMatch1 = - new RouteMatch( - pathMatcher, - Collections.emptyList(), - new FractionMatcher(100, 1000, new FakeRandom(50))); - assertThat(routeMatch1.matches("/FooService/barMethod", headers)).isTrue(); - - RouteMatch routeMatch2 = - new RouteMatch( - pathMatcher, - Collections.emptyList(), - new FractionMatcher(100, 1000, new FakeRandom(100))); - assertThat(routeMatch2.matches("/FooService/barMethod", headers)).isFalse(); - } - - @Test - public void headerMatching_specialCaseGrpcHeaders() { - PathMatcher pathMatcher = PathMatcher.fromPath("/FooService/barMethod", true); - Map> headers = new HashMap<>(); - headers.put("grpc-previous-rpc-attempts", Collections.singletonList("0")); - - RouteMatch routeMatch1 = - new RouteMatch(pathMatcher, - Arrays.asList( - new HeaderMatcher( - "grpc-previous-rpc-attempts", "0", null, null, null, null, - null, false)), - null); - assertThat(routeMatch1.matches("/FooService/barMethod", headers)).isFalse(); - - RouteMatch routeMatch2 = - new RouteMatch(pathMatcher, - Arrays.asList( - new HeaderMatcher( - "content-type", "application/grpc", null, null, null, null, - null, false)), - null); - assertThat(routeMatch2.matches("/FooService/barMethod", headers)).isTrue(); - } - - private static final class FakeRandom implements ThreadSafeRandom { - private final int value; - - FakeRandom(int value) { - this.value = value; - } - - @Override - public int nextInt(int bound) { - return value; - } - } -} diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 219a4aee5f..7eeea86e9a 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -18,6 +18,7 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -26,6 +27,7 @@ import static org.mockito.Mockito.when; import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; +import com.google.re2j.Pattern; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; @@ -49,15 +51,18 @@ import io.grpc.internal.NoopClientCall.NoopClientCallListener; import io.grpc.internal.ObjectPool; import io.grpc.internal.PickSubchannelArgsImpl; import io.grpc.testing.TestMethodDescriptors; -import io.grpc.xds.EnvoyProtoData.ClusterWeight; -import io.grpc.xds.EnvoyProtoData.Route; -import io.grpc.xds.EnvoyProtoData.RouteAction; -import io.grpc.xds.EnvoyProtoData.VirtualHost; +import io.grpc.xds.Matchers.HeaderMatcher; +import io.grpc.xds.Matchers.PathMatcher; +import io.grpc.xds.VirtualHost.Route; +import io.grpc.xds.VirtualHost.Route.RouteAction; +import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight; +import io.grpc.xds.VirtualHost.Route.RouteMatch; import io.grpc.xds.XdsClient.RdsResourceWatcher; import io.grpc.xds.XdsNameResolverProvider.XdsClientPoolFactory; import java.io.IOException; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -232,25 +237,25 @@ public class XdsNameResolverTest { } private List buildUnmatchedVirtualHosts() { - Route route1 = new Route(RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), - new RouteAction(TimeUnit.SECONDS.toNanos(15L), cluster2, null)); - Route route2 = new Route(RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), - new RouteAction(TimeUnit.SECONDS.toNanos(15L), cluster1, null)); + Route route1 = Route.create(RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), + RouteAction.forCluster(cluster2, TimeUnit.SECONDS.toNanos(15L)), null); + Route route2 = Route.create(RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), + RouteAction.forCluster(cluster1, TimeUnit.SECONDS.toNanos(15L)), null); return Arrays.asList( - new VirtualHost("virtualhost-foo", Collections.singletonList("hello.googleapis.com"), - Collections.singletonList(route1)), - new VirtualHost("virtualhost-bar", Collections.singletonList("hi.googleapis.com"), - Collections.singletonList(route2))); + VirtualHost.create("virtualhost-foo", Collections.singletonList("hello.googleapis.com"), + Collections.singletonList(route1), null), + VirtualHost.create("virtualhost-bar", Collections.singletonList("hi.googleapis.com"), + Collections.singletonList(route2), null)); } @Test public void resolved_noTimeout() { resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); - Route route = new Route(RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), - new RouteAction(null, cluster1, null)); // per-route timeout unset - VirtualHost virtualHost = new VirtualHost("does not matter", - Collections.singletonList(AUTHORITY), Collections.singletonList(route)); + Route route = Route.create(RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), + RouteAction.forCluster(cluster1, null), null); // per-route timeout unset + VirtualHost virtualHost = VirtualHost.create("does not matter", + Collections.singletonList(AUTHORITY), Collections.singletonList(route), null); xdsClient.deliverLdsUpdate(AUTHORITY, 0L, Collections.singletonList(virtualHost)); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); @@ -262,10 +267,10 @@ public class XdsNameResolverTest { public void resolved_fallbackToHttpMaxStreamDurationAsTimeout() { resolver.start(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); - Route route = new Route(RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), - new RouteAction(null, cluster1, null)); // per-route timeout unset - VirtualHost virtualHost = new VirtualHost("does not matter", - Collections.singletonList(AUTHORITY), Collections.singletonList(route)); + Route route = Route.create(RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), + RouteAction.forCluster(cluster1, null), null); // per-route timeout unset + VirtualHost virtualHost = VirtualHost.create("does not matter", + Collections.singletonList(AUTHORITY), Collections.singletonList(route), null); xdsClient.deliverLdsUpdate(AUTHORITY, TimeUnit.SECONDS.toNanos(5L), Collections.singletonList(virtualHost)); verify(mockListener).onResult(resolutionResultCaptor.capture()); @@ -307,12 +312,12 @@ public class XdsNameResolverTest { xdsClient.deliverLdsUpdate( AUTHORITY, Arrays.asList( - new Route( + Route.create( RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), - new RouteAction(TimeUnit.SECONDS.toNanos(20L), "another-cluster", null)), - new Route( + RouteAction.forCluster("another-cluster", TimeUnit.SECONDS.toNanos(20L)), null), + Route.create( RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), - new RouteAction(TimeUnit.SECONDS.toNanos(15L), cluster2, null)))); + RouteAction.forCluster(cluster2, TimeUnit.SECONDS.toNanos(15L)), null))); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); // Updated service config still contains cluster1 while it is removed resource. New calls no @@ -342,12 +347,12 @@ public class XdsNameResolverTest { xdsClient.deliverLdsUpdate( AUTHORITY, Arrays.asList( - new Route( + Route.create( RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), - new RouteAction(TimeUnit.SECONDS.toNanos(20L), "another-cluster", null)), - new Route( + RouteAction.forCluster("another-cluster", TimeUnit.SECONDS.toNanos(20L)), null), + Route.create( RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), - new RouteAction(TimeUnit.SECONDS.toNanos(15L), cluster2, null)))); + RouteAction.forCluster(cluster2, TimeUnit.SECONDS.toNanos(15L)), null))); // Two consecutive service config updates: one for removing clcuster1, // one for adding "another=cluster". verify(mockListener, times(2)).onResult(resolutionResultCaptor.capture()); @@ -373,12 +378,12 @@ public class XdsNameResolverTest { xdsClient.deliverLdsUpdate( AUTHORITY, Arrays.asList( - new Route( + Route.create( RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), - new RouteAction(TimeUnit.SECONDS.toNanos(20L), "another-cluster", null)), - new Route( + RouteAction.forCluster("another-cluster", TimeUnit.SECONDS.toNanos(20L)), null), + Route.create( RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), - new RouteAction(TimeUnit.SECONDS.toNanos(15L), cluster2, null)))); + RouteAction.forCluster(cluster2, TimeUnit.SECONDS.toNanos(15L)), null))); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); @@ -389,12 +394,12 @@ public class XdsNameResolverTest { xdsClient.deliverLdsUpdate( AUTHORITY, Arrays.asList( - new Route( + Route.create( RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), - new RouteAction(TimeUnit.SECONDS.toNanos(15L), "another-cluster", null)), - new Route( + RouteAction.forCluster("another-cluster", TimeUnit.SECONDS.toNanos(15L)), null), + Route.create( RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), - new RouteAction(TimeUnit.SECONDS.toNanos(15L), cluster2, null)))); + RouteAction.forCluster(cluster2, TimeUnit.SECONDS.toNanos(15L)), null))); verifyNoMoreInteractions(mockListener); // no cluster added/deleted assertCallSelectResult(call1, configSelector, "another-cluster", 15.0); } @@ -407,18 +412,18 @@ public class XdsNameResolverTest { xdsClient.deliverLdsUpdate( AUTHORITY, Collections.singletonList( - new Route( + Route.create( RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), - new RouteAction(TimeUnit.SECONDS.toNanos(15L), cluster2, null)))); + RouteAction.forCluster(cluster2, TimeUnit.SECONDS.toNanos(15L)), null))); xdsClient.deliverLdsUpdate( AUTHORITY, Arrays.asList( - new Route( + Route.create( RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), - new RouteAction(TimeUnit.SECONDS.toNanos(15L), cluster1, null)), - new Route( + RouteAction.forCluster(cluster1, TimeUnit.SECONDS.toNanos(15L)), null), + Route.create( RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), - new RouteAction(TimeUnit.SECONDS.toNanos(15L), cluster2, null)))); + RouteAction.forCluster(cluster2, TimeUnit.SECONDS.toNanos(15L)), null))); testCall.deliverErrorStatus(); verifyNoMoreInteractions(mockListener); } @@ -431,14 +436,14 @@ public class XdsNameResolverTest { FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate( AUTHORITY, - Arrays.asList( - new Route( + Collections.singletonList( + Route.create( RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), - new RouteAction( - TimeUnit.SECONDS.toNanos(20L), null, + RouteAction.forWeightedClusters( Arrays.asList( - new ClusterWeight(cluster1, 20, null), - new ClusterWeight(cluster2, 80, null)))))); + ClusterWeight.create(cluster1, 20, null), + ClusterWeight.create(cluster2, 80, null)), + TimeUnit.SECONDS.toNanos(20L)), null))); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); assertThat(result.getAddresses()).isEmpty(); @@ -493,12 +498,12 @@ public class XdsNameResolverTest { xdsClient.deliverLdsUpdate( AUTHORITY, Arrays.asList( - new Route( + Route.create( RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), - new RouteAction(TimeUnit.SECONDS.toNanos(15L), cluster1, null)), - new Route( + RouteAction.forCluster(cluster1, TimeUnit.SECONDS.toNanos(15L)), null), + Route.create( RouteMatch.withPathExactOnly(call2.getFullMethodNameForPath()), - new RouteAction(TimeUnit.SECONDS.toNanos(15L), cluster2, null)))); + RouteAction.forCluster(cluster2, TimeUnit.SECONDS.toNanos(15L)), null))); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); assertThat(result.getAddresses()).isEmpty(); @@ -636,12 +641,12 @@ public class XdsNameResolverTest { public void findVirtualHostForHostName_exactMatchFirst() { String hostname = "a.googleapis.com"; List routes = Collections.emptyList(); - VirtualHost vHost1 = new VirtualHost("virtualhost01.googleapis.com", - Arrays.asList("a.googleapis.com", "b.googleapis.com"), routes); - VirtualHost vHost2 = new VirtualHost("virtualhost02.googleapis.com", - Collections.singletonList("*.googleapis.com"), routes); - VirtualHost vHost3 = - new VirtualHost("virtualhost03.googleapis.com", Collections.singletonList("*"), routes); + VirtualHost vHost1 = VirtualHost.create("virtualhost01.googleapis.com", + Arrays.asList("a.googleapis.com", "b.googleapis.com"), routes, null); + VirtualHost vHost2 = VirtualHost.create("virtualhost02.googleapis.com", + Collections.singletonList("*.googleapis.com"), routes, null); + VirtualHost vHost3 = VirtualHost.create("virtualhost03.googleapis.com", + Collections.singletonList("*"), routes, null); List virtualHosts = Arrays.asList(vHost1, vHost2, vHost3); assertThat(XdsNameResolver.findVirtualHostForHostName(virtualHosts, hostname)) .isEqualTo(vHost1); @@ -651,14 +656,12 @@ public class XdsNameResolverTest { public void findVirtualHostForHostName_preferSuffixDomainOverPrefixDomain() { String hostname = "a.googleapis.com"; List routes = Collections.emptyList(); - VirtualHost vHost1 = - new VirtualHost("virtualhost01.googleapis.com", - Arrays.asList("*.googleapis.com", "b.googleapis.com"), routes); - VirtualHost vHost2 = - new VirtualHost("virtualhost02.googleapis.com", - Collections.singletonList("a.googleapis.*"), routes); - VirtualHost vHost3 = - new VirtualHost("virtualhost03.googleapis.com", Collections.singletonList("*"), routes); + VirtualHost vHost1 = VirtualHost.create("virtualhost01.googleapis.com", + Arrays.asList("*.googleapis.com", "b.googleapis.com"), routes, null); + VirtualHost vHost2 = VirtualHost.create("virtualhost02.googleapis.com", + Collections.singletonList("a.googleapis.*"), routes, null); + VirtualHost vHost3 = VirtualHost.create("virtualhost03.googleapis.com", + Collections.singletonList("*"), routes, null); List virtualHosts = Arrays.asList(vHost1, vHost2, vHost3); assertThat(XdsNameResolver.findVirtualHostForHostName(virtualHosts, hostname)) .isEqualTo(vHost1); @@ -668,16 +671,127 @@ public class XdsNameResolverTest { public void findVirtualHostForHostName_asteriskMatchAnyDomain() { String hostname = "a.googleapis.com"; List routes = Collections.emptyList(); - VirtualHost vHost1 = - new VirtualHost("virtualhost01.googleapis.com", Collections.singletonList("*"), routes); - VirtualHost vHost2 = - new VirtualHost("virtualhost02.googleapis.com", - Collections.singletonList("b.googleapis.com"), routes); + VirtualHost vHost1 = VirtualHost.create("virtualhost01.googleapis.com", + Collections.singletonList("*"), routes, null); + VirtualHost vHost2 = VirtualHost.create("virtualhost02.googleapis.com", + Collections.singletonList("b.googleapis.com"), routes, null); List virtualHosts = Arrays.asList(vHost1, vHost2); assertThat(XdsNameResolver.findVirtualHostForHostName(virtualHosts, hostname)) .isEqualTo(vHost1);; } + @Test + public void routeMatching_pathOnly() { + Map> headers = Collections.emptyMap(); + ThreadSafeRandom random = mock(ThreadSafeRandom.class); + + RouteMatch routeMatch1 = + RouteMatch.create( + PathMatcher.fromPath("/FooService/barMethod", true), + Collections.emptyList(), null); + assertThat(XdsNameResolver.matchRoute(routeMatch1, "/FooService/barMethod", headers, random)) + .isTrue(); + assertThat(XdsNameResolver.matchRoute(routeMatch1, "/FooService/bazMethod", headers, random)) + .isFalse(); + + RouteMatch routeMatch2 = + RouteMatch.create( + PathMatcher.fromPrefix("/FooService/", true), + Collections.emptyList(), null); + assertThat(XdsNameResolver.matchRoute(routeMatch2, "/FooService/barMethod", headers, random)) + .isTrue(); + assertThat(XdsNameResolver.matchRoute(routeMatch2, "/FooService/bazMethod", headers, random)) + .isTrue(); + assertThat(XdsNameResolver.matchRoute(routeMatch2, "/BarService/bazMethod", headers, random)) + .isFalse(); + + RouteMatch routeMatch3 = + RouteMatch.create( + PathMatcher.fromRegEx(Pattern.compile(".*Foo.*")), + Collections.emptyList(), null); + assertThat(XdsNameResolver.matchRoute(routeMatch3, "/FooService/barMethod", headers, random)) + .isTrue(); + } + + @Test + public void routeMatching_withHeaders() { + Map> headers = new HashMap<>(); + headers.put("authority", Collections.singletonList("foo.googleapis.com")); + headers.put("grpc-encoding", Collections.singletonList("gzip")); + headers.put("user-agent", Collections.singletonList("gRPC-Java")); + headers.put("content-length", Collections.singletonList("1000")); + headers.put("custom-key", Arrays.asList("custom-value1", "custom-value2")); + ThreadSafeRandom random = mock(ThreadSafeRandom.class); + + PathMatcher pathMatcher = PathMatcher.fromPath("/FooService/barMethod", true); + RouteMatch routeMatch1 = RouteMatch.create( + pathMatcher, + Arrays.asList( + HeaderMatcher.forExactValue("grpc-encoding", "gzip", false), + HeaderMatcher.forSafeRegEx("authority", Pattern.compile(".*googleapis.*"), false), + HeaderMatcher.forRange( + "content-length", HeaderMatcher.Range.create(100, 10000), false), + HeaderMatcher.forPresent("user-agent", true, false), + HeaderMatcher.forPrefix("custom-key", "custom-", false), + HeaderMatcher.forSuffix("custom-key", "value2", false)), + null); + assertThat(XdsNameResolver.matchRoute(routeMatch1, "/FooService/barMethod", headers, random)) + .isTrue(); + + RouteMatch routeMatch2 = RouteMatch.create( + pathMatcher, + Collections.singletonList( + HeaderMatcher.forSafeRegEx("authority", Pattern.compile(".*googleapis.*"), true)), + null); + assertThat(XdsNameResolver.matchRoute(routeMatch2, "/FooService/barMethod", headers, random)) + .isFalse(); + + RouteMatch routeMatch3 = RouteMatch.create( + pathMatcher, + Collections.singletonList( + HeaderMatcher.forExactValue("user-agent", "gRPC-Go", false)), null); + assertThat(XdsNameResolver.matchRoute(routeMatch3, "/FooService/barMethod", headers, random)) + .isFalse(); + + RouteMatch routeMatch4 = RouteMatch.create( + pathMatcher, + Collections.singletonList(HeaderMatcher.forPresent("user-agent", false, false)), + null); + assertThat(XdsNameResolver.matchRoute(routeMatch4, "/FooService/barMethod", headers, random)) + .isFalse(); + + RouteMatch routeMatch5 = RouteMatch.create( + pathMatcher, + Collections.singletonList(HeaderMatcher.forPresent("user-agent", false, true)), // inverted + null); + assertThat(XdsNameResolver.matchRoute(routeMatch5, "/FooService/barMethod", headers, random)) + .isTrue(); + + RouteMatch routeMatch6 = RouteMatch.create( + pathMatcher, + Collections.singletonList(HeaderMatcher.forPresent("user-agent", true, true)), + null); + assertThat(XdsNameResolver.matchRoute(routeMatch6, "/FooService/barMethod", headers, random)) + .isFalse(); + + RouteMatch routeMatch7 = RouteMatch.create( + pathMatcher, + Collections.singletonList( + HeaderMatcher.forExactValue("custom-key", "custom-value1,custom-value2", false)), + null); + assertThat(XdsNameResolver.matchRoute(routeMatch7, "/FooService/barMethod", headers, random)) + .isTrue(); + } + + @Test + public void pathMatching_caseInsensitive() { + PathMatcher pathMatcher1 = PathMatcher.fromPath("/FooService/barMethod", false); + assertThat(XdsNameResolver.matchPath(pathMatcher1, "/fooservice/barmethod")).isTrue(); + + PathMatcher pathMatcher2 = PathMatcher.fromPrefix("/FooService", false); + assertThat(XdsNameResolver.matchPath(pathMatcher2, "/fooservice/barmethod")).isTrue(); + } + private final class FakeXdsClientPoolFactory implements XdsClientPoolFactory { @Override @@ -757,8 +871,8 @@ public class XdsNameResolverTest { if (!resourceName.equals(ldsResource)) { return; } - VirtualHost virtualHost = - new VirtualHost("virtual-host", Collections.singletonList(AUTHORITY), routes); + VirtualHost virtualHost = VirtualHost.create("virtual-host", + Collections.singletonList(AUTHORITY), routes, null); ldsWatcher.onChanged( new LdsUpdate(0, Collections.singletonList(virtualHost), false, null)); }