diff --git a/core/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java b/core/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java index 3fb32fdb07..66380b9631 100644 --- a/core/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java +++ b/core/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java @@ -31,6 +31,7 @@ import io.grpc.ClientStreamTracer.StreamInfo; import io.grpc.ConnectivityState; import io.grpc.ConnectivityStateInfo; import io.grpc.EquivalentAddressGroup; +import io.grpc.Internal; import io.grpc.LoadBalancer; import io.grpc.Metadata; import io.grpc.Status; @@ -58,7 +59,8 @@ import javax.annotation.Nullable; *

This implements the outlier detection gRFC: * https://github.com/grpc/proposal/blob/master/A50-xds-outlier-detection.md */ -public class OutlierDetectionLoadBalancer extends LoadBalancer { +@Internal +public final class OutlierDetectionLoadBalancer extends LoadBalancer { @VisibleForTesting final AddressTrackerMap trackerMap; @@ -837,13 +839,13 @@ public class OutlierDetectionLoadBalancer extends LoadBalancer { */ public static final class OutlierDetectionLoadBalancerConfig { - final Long intervalNanos; - final Long baseEjectionTimeNanos; - final Long maxEjectionTimeNanos; - final Integer maxEjectionPercent; - final SuccessRateEjection successRateEjection; - final FailurePercentageEjection failurePercentageEjection; - final PolicySelection childPolicy; + public final Long intervalNanos; + public final Long baseEjectionTimeNanos; + public final Long maxEjectionTimeNanos; + public final Integer maxEjectionPercent; + public final SuccessRateEjection successRateEjection; + public final FailurePercentageEjection failurePercentageEjection; + public final PolicySelection childPolicy; private OutlierDetectionLoadBalancerConfig(Long intervalNanos, Long baseEjectionTimeNanos, @@ -932,10 +934,10 @@ public class OutlierDetectionLoadBalancer extends LoadBalancer { /** The configuration for success rate ejection. */ public static class SuccessRateEjection { - final Integer stdevFactor; - final Integer enforcementPercentage; - final Integer minimumHosts; - final Integer requestVolume; + public final Integer stdevFactor; + public final Integer enforcementPercentage; + public final Integer minimumHosts; + public final Integer requestVolume; SuccessRateEjection(Integer stdevFactor, Integer enforcementPercentage, Integer minimumHosts, Integer requestVolume) { @@ -996,10 +998,10 @@ public class OutlierDetectionLoadBalancer extends LoadBalancer { /** The configuration for failure percentage ejection. */ public static class FailurePercentageEjection { - final Integer threshold; - final Integer enforcementPercentage; - final Integer minimumHosts; - final Integer requestVolume; + public final Integer threshold; + public final Integer enforcementPercentage; + public final Integer minimumHosts; + public final Integer requestVolume; FailurePercentageEjection(Integer threshold, Integer enforcementPercentage, Integer minimumHosts, Integer requestVolume) { diff --git a/core/src/main/java/io/grpc/util/OutlierDetectionLoadBalancerProvider.java b/core/src/main/java/io/grpc/util/OutlierDetectionLoadBalancerProvider.java index a92f49bd1d..e52c741465 100644 --- a/core/src/main/java/io/grpc/util/OutlierDetectionLoadBalancerProvider.java +++ b/core/src/main/java/io/grpc/util/OutlierDetectionLoadBalancerProvider.java @@ -16,6 +16,7 @@ package io.grpc.util; +import io.grpc.Internal; import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancerProvider; @@ -33,6 +34,7 @@ import io.grpc.util.OutlierDetectionLoadBalancer.OutlierDetectionLoadBalancerCon import java.util.List; import java.util.Map; +@Internal public final class OutlierDetectionLoadBalancerProvider extends LoadBalancerProvider { @Override diff --git a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java index 4a599ef232..4fa701c3c4 100644 --- a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java +++ b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java @@ -159,7 +159,7 @@ final class CdsLoadBalancer2 extends LoadBalancer { instance = DiscoveryMechanism.forEds( clusterState.name, clusterState.result.edsServiceName(), clusterState.result.lrsServerInfo(), clusterState.result.maxConcurrentRequests(), - clusterState.result.upstreamTlsContext()); + clusterState.result.upstreamTlsContext(), clusterState.result.outlierDetection()); } else { // logical DNS instance = DiscoveryMechanism.forLogicalDns( clusterState.name, clusterState.result.dnsHostName(), diff --git a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java index 9a4c366f84..208b9a2852 100644 --- a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java +++ b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java @@ -90,6 +90,7 @@ import io.grpc.xds.EnvoyServerProtoData.CidrRange; import io.grpc.xds.EnvoyServerProtoData.ConnectionSourceType; import io.grpc.xds.EnvoyServerProtoData.FilterChain; import io.grpc.xds.EnvoyServerProtoData.FilterChainMatch; +import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.Filter.ClientInterceptorBuilder; import io.grpc.xds.Filter.FilterConfig; @@ -166,6 +167,10 @@ final class ClientXdsClient extends XdsClient implements XdsResponseHandler, Res static boolean enableCustomLbConfig = Strings.isNullOrEmpty(System.getenv("GRPC_EXPERIMENTAL_XDS_CUSTOM_LB_CONFIG")) || Boolean.parseBoolean(System.getenv("GRPC_EXPERIMENTAL_XDS_CUSTOM_LB_CONFIG")); + @VisibleForTesting + static boolean enableOutlierDetection = + !Strings.isNullOrEmpty(System.getenv("GRPC_EXPERIMENTAL_XDS_OUTLIER_DETECTION")) + || Boolean.parseBoolean(System.getenv("GRPC_EXPERIMENTAL_XDS_OUTLIER_DETECTION")); private static final String TYPE_URL_HTTP_CONNECTION_MANAGER_V2 = "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2" + ".HttpConnectionManager"; @@ -632,6 +637,65 @@ final class ClientXdsClient extends XdsClient implements XdsResponseHandler, Res } } + static io.envoyproxy.envoy.config.cluster.v3.OutlierDetection validateOutlierDetection( + io.envoyproxy.envoy.config.cluster.v3.OutlierDetection outlierDetection) + throws ResourceInvalidException { + if (outlierDetection.hasInterval()) { + if (!Durations.isValid(outlierDetection.getInterval())) { + throw new ResourceInvalidException("outlier_detection interval is not a valid Duration"); + } + if (hasNegativeValues(outlierDetection.getInterval())) { + throw new ResourceInvalidException("outlier_detection interval has a negative value"); + } + } + if (outlierDetection.hasBaseEjectionTime()) { + if (!Durations.isValid(outlierDetection.getBaseEjectionTime())) { + throw new ResourceInvalidException( + "outlier_detection base_ejection_time is not a valid Duration"); + } + if (hasNegativeValues(outlierDetection.getBaseEjectionTime())) { + throw new ResourceInvalidException( + "outlier_detection base_ejection_time has a negative value"); + } + } + if (outlierDetection.hasMaxEjectionTime()) { + if (!Durations.isValid(outlierDetection.getMaxEjectionTime())) { + throw new ResourceInvalidException( + "outlier_detection max_ejection_time is not a valid Duration"); + } + if (hasNegativeValues(outlierDetection.getMaxEjectionTime())) { + throw new ResourceInvalidException( + "outlier_detection max_ejection_time has a negative value"); + } + } + if (outlierDetection.hasMaxEjectionPercent() + && outlierDetection.getMaxEjectionPercent().getValue() > 100) { + throw new ResourceInvalidException( + "outlier_detection max_ejection_percent is > 100"); + } + if (outlierDetection.hasEnforcingSuccessRate() + && outlierDetection.getEnforcingSuccessRate().getValue() > 100) { + throw new ResourceInvalidException( + "outlier_detection enforcing_success_rate is > 100"); + } + if (outlierDetection.hasFailurePercentageThreshold() + && outlierDetection.getFailurePercentageThreshold().getValue() > 100) { + throw new ResourceInvalidException( + "outlier_detection failure_percentage_threshold is > 100"); + } + if (outlierDetection.hasEnforcingFailurePercentage() + && outlierDetection.getEnforcingFailurePercentage().getValue() > 100) { + throw new ResourceInvalidException( + "outlier_detection enforcing_failure_percentage is > 100"); + } + + return outlierDetection; + } + + static boolean hasNegativeValues(Duration duration) { + return duration.getSeconds() < 0 || duration.getNanos() < 0; + } + private static String getIdentityCertInstanceName(CommonTlsContext commonTlsContext) { if (commonTlsContext.hasTlsCertificateProviderInstance()) { return commonTlsContext.getTlsCertificateProviderInstance().getInstanceName(); @@ -1704,6 +1768,7 @@ final class ClientXdsClient extends XdsClient implements XdsResponseHandler, Res ServerInfo lrsServerInfo = null; Long maxConcurrentRequests = null; UpstreamTlsContext upstreamTlsContext = null; + OutlierDetection outlierDetection = null; if (cluster.hasLrsServer()) { if (!cluster.getLrsServer().hasSelf()) { return StructOrError.fromError( @@ -1743,6 +1808,16 @@ final class ClientXdsClient extends XdsClient implements XdsResponseHandler, Res "Cluster " + clusterName + ": malformed UpstreamTlsContext: " + e); } } + if (cluster.hasOutlierDetection() && enableOutlierDetection) { + try { + outlierDetection = OutlierDetection.fromEnvoyOutlierDetection( + validateOutlierDetection(cluster.getOutlierDetection())); + } catch (ResourceInvalidException e) { + return StructOrError.fromError( + "Cluster " + clusterName + ": malformed outlier_detection: " + e); + } + } + DiscoveryType type = cluster.getType(); if (type == DiscoveryType.EDS) { @@ -1763,7 +1838,8 @@ final class ClientXdsClient extends XdsClient implements XdsResponseHandler, Res edsResources.add(clusterName); } return StructOrError.fromStruct(CdsUpdate.forEds( - clusterName, edsServiceName, lrsServerInfo, maxConcurrentRequests, upstreamTlsContext)); + clusterName, edsServiceName, lrsServerInfo, maxConcurrentRequests, upstreamTlsContext, + outlierDetection)); } else if (type.equals(DiscoveryType.LOGICAL_DNS)) { if (!cluster.hasLoadAssignment()) { return StructOrError.fromError( diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java index 91da43eb24..96e2cf441a 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java @@ -38,6 +38,7 @@ import io.grpc.internal.ObjectPool; import io.grpc.internal.ServiceConfigUtil.PolicySelection; import io.grpc.util.ForwardingLoadBalancerHelper; import io.grpc.util.GracefulSwitchLoadBalancer; +import io.grpc.util.OutlierDetectionLoadBalancer.OutlierDetectionLoadBalancerConfig; import io.grpc.xds.Bootstrapper.ServerInfo; import io.grpc.xds.ClusterImplLoadBalancerProvider.ClusterImplConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig; @@ -45,6 +46,9 @@ import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig.Dis import io.grpc.xds.Endpoints.DropOverload; import io.grpc.xds.Endpoints.LbEndpoint; import io.grpc.xds.Endpoints.LocalityLbEndpoints; +import io.grpc.xds.EnvoyServerProtoData.FailurePercentageEjection; +import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; +import io.grpc.xds.EnvoyServerProtoData.SuccessRateEjection; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; @@ -176,7 +180,8 @@ final class ClusterResolverLoadBalancer extends LoadBalancer { ClusterState state; if (instance.type == DiscoveryMechanism.Type.EDS) { state = new EdsClusterState(instance.cluster, instance.edsServiceName, - instance.lrsServerInfo, instance.maxConcurrentRequests, instance.tlsContext); + instance.lrsServerInfo, instance.maxConcurrentRequests, instance.tlsContext, + instance.outlierDetection); } else { // logical DNS state = new LogicalDnsClusterState(instance.cluster, instance.dnsHostName, instance.lrsServerInfo, instance.maxConcurrentRequests, instance.tlsContext); @@ -316,6 +321,8 @@ final class ClusterResolverLoadBalancer extends LoadBalancer { protected final Long maxConcurrentRequests; @Nullable protected final UpstreamTlsContext tlsContext; + @Nullable + protected final OutlierDetection outlierDetection; // Resolution status, may contain most recent error encountered. protected Status status = Status.OK; // True if has received resolution result. @@ -327,11 +334,13 @@ final class ClusterResolverLoadBalancer extends LoadBalancer { protected boolean shutdown; private ClusterState(String name, @Nullable ServerInfo lrsServerInfo, - @Nullable Long maxConcurrentRequests, @Nullable UpstreamTlsContext tlsContext) { + @Nullable Long maxConcurrentRequests, @Nullable UpstreamTlsContext tlsContext, + @Nullable OutlierDetection outlierDetection) { this.name = name; this.lrsServerInfo = lrsServerInfo; this.maxConcurrentRequests = maxConcurrentRequests; this.tlsContext = tlsContext; + this.outlierDetection = outlierDetection; } abstract void start(); @@ -349,8 +358,8 @@ final class ClusterResolverLoadBalancer extends LoadBalancer { private EdsClusterState(String name, @Nullable String edsServiceName, @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, - @Nullable UpstreamTlsContext tlsContext) { - super(name, lrsServerInfo, maxConcurrentRequests, tlsContext); + @Nullable UpstreamTlsContext tlsContext, @Nullable OutlierDetection outlierDetection) { + super(name, lrsServerInfo, maxConcurrentRequests, tlsContext, outlierDetection); this.edsServiceName = edsServiceName; } @@ -434,7 +443,8 @@ final class ClusterResolverLoadBalancer extends LoadBalancer { Map priorityChildConfigs = generateEdsBasedPriorityChildConfigs( name, edsServiceName, lrsServerInfo, maxConcurrentRequests, tlsContext, - endpointLbPolicy, lbRegistry, prioritizedLocalityWeights, dropOverloads); + outlierDetection, endpointLbPolicy, lbRegistry, prioritizedLocalityWeights, + dropOverloads); status = Status.OK; resolved = true; result = new ClusterResolutionResult(addresses, priorityChildConfigs, @@ -530,7 +540,7 @@ final class ClusterResolverLoadBalancer extends LoadBalancer { private LogicalDnsClusterState(String name, String dnsHostName, @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, @Nullable UpstreamTlsContext tlsContext) { - super(name, lrsServerInfo, maxConcurrentRequests, tlsContext); + super(name, lrsServerInfo, maxConcurrentRequests, tlsContext, null); this.dnsHostName = checkNotNull(dnsHostName, "dnsHostName"); nameResolverFactory = checkNotNull(helper.getNameResolverRegistry().asFactory(), "nameResolverFactory"); @@ -730,9 +740,9 @@ final class ClusterResolverLoadBalancer extends LoadBalancer { private static Map generateEdsBasedPriorityChildConfigs( String cluster, @Nullable String edsServiceName, @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, @Nullable UpstreamTlsContext tlsContext, - PolicySelection endpointLbPolicy, LoadBalancerRegistry lbRegistry, - Map> prioritizedLocalityWeights, - List dropOverloads) { + @Nullable OutlierDetection outlierDetection, PolicySelection endpointLbPolicy, + LoadBalancerRegistry lbRegistry, Map> prioritizedLocalityWeights, List dropOverloads) { Map configs = new HashMap<>(); for (String priority : prioritizedLocalityWeights.keySet()) { ClusterImplConfig clusterImplConfig = @@ -740,15 +750,98 @@ final class ClusterResolverLoadBalancer extends LoadBalancer { dropOverloads, endpointLbPolicy, tlsContext); LoadBalancerProvider clusterImplLbProvider = lbRegistry.getProvider(XdsLbPolicies.CLUSTER_IMPL_POLICY_NAME); - PolicySelection clusterImplPolicy = + PolicySelection priorityChildPolicy = new PolicySelection(clusterImplLbProvider, clusterImplConfig); + + // If outlier detection has been configured we wrap the child policy in the outlier detection + // load balancer. + if (outlierDetection != null) { + LoadBalancerProvider outlierDetectionProvider = lbRegistry.getProvider( + "outlier_detection_experimental"); + priorityChildPolicy = new PolicySelection(outlierDetectionProvider, + buildOutlierDetectionLbConfig(outlierDetection, priorityChildPolicy)); + } + PriorityChildConfig priorityChildConfig = - new PriorityChildConfig(clusterImplPolicy, true /* ignoreReresolution */); + new PriorityChildConfig(priorityChildPolicy, true /* ignoreReresolution */); configs.put(priority, priorityChildConfig); } return configs; } + /** + * Converts {@link OutlierDetection} that represents the xDS configuration to {@link + * OutlierDetectionLoadBalancerConfig} that the {@link io.grpc.util.OutlierDetectionLoadBalancer} + * understands. + */ + private static OutlierDetectionLoadBalancerConfig buildOutlierDetectionLbConfig( + OutlierDetection outlierDetection, PolicySelection childPolicy) { + OutlierDetectionLoadBalancerConfig.Builder configBuilder + = new OutlierDetectionLoadBalancerConfig.Builder(); + + configBuilder.setChildPolicy(childPolicy); + + if (outlierDetection.intervalNanos() != null) { + configBuilder.setIntervalNanos(outlierDetection.intervalNanos()); + } + if (outlierDetection.baseEjectionTimeNanos() != null) { + configBuilder.setBaseEjectionTimeNanos(outlierDetection.baseEjectionTimeNanos()); + } + if (outlierDetection.maxEjectionTimeNanos() != null) { + configBuilder.setMaxEjectionTimeNanos(outlierDetection.maxEjectionTimeNanos()); + } + if (outlierDetection.maxEjectionPercent() != null) { + configBuilder.setMaxEjectionPercent(outlierDetection.maxEjectionPercent()); + } + + SuccessRateEjection successRate = outlierDetection.successRateEjection(); + if (successRate != null) { + OutlierDetectionLoadBalancerConfig.SuccessRateEjection.Builder + successRateConfigBuilder = new OutlierDetectionLoadBalancerConfig + .SuccessRateEjection.Builder(); + + if (successRate.stdevFactor() != null) { + successRateConfigBuilder.setStdevFactor(successRate.stdevFactor()); + } + if (successRate.enforcementPercentage() != null) { + successRateConfigBuilder.setEnforcementPercentage(successRate.enforcementPercentage()); + } + if (successRate.minimumHosts() != null) { + successRateConfigBuilder.setMinimumHosts(successRate.minimumHosts()); + } + if (successRate.requestVolume() != null) { + successRateConfigBuilder.setRequestVolume(successRate.requestVolume()); + } + + configBuilder.setSuccessRateEjection(successRateConfigBuilder.build()); + } + + FailurePercentageEjection failurePercentage = outlierDetection.failurePercentageEjection(); + if (failurePercentage != null) { + OutlierDetectionLoadBalancerConfig.FailurePercentageEjection.Builder + failurePercentageConfigBuilder = new OutlierDetectionLoadBalancerConfig + .FailurePercentageEjection.Builder(); + + if (failurePercentage.threshold() != null) { + failurePercentageConfigBuilder.setThreshold(failurePercentage.threshold()); + } + if (failurePercentage.enforcementPercentage() != null) { + failurePercentageConfigBuilder.setEnforcementPercentage( + failurePercentage.enforcementPercentage()); + } + if (failurePercentage.minimumHosts() != null) { + failurePercentageConfigBuilder.setMinimumHosts(failurePercentage.minimumHosts()); + } + if (failurePercentage.requestVolume() != null) { + failurePercentageConfigBuilder.setRequestVolume(failurePercentage.requestVolume()); + } + + configBuilder.setFailurePercentageEjection(failurePercentageConfigBuilder.build()); + } + + return configBuilder.build(); + } + /** * Generates a string that represents the priority in the LB policy config. The string is unique * across priorities in all clusters and priorityName(c, p1) < priorityName(c, p2) iff p1 < p2. diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java index 6f6f887e92..38da1f465c 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java @@ -26,6 +26,7 @@ import io.grpc.LoadBalancerProvider; import io.grpc.NameResolver.ConfigOrError; import io.grpc.internal.ServiceConfigUtil.PolicySelection; import io.grpc.xds.Bootstrapper.ServerInfo; +import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import java.util.List; import java.util.Map; @@ -124,6 +125,8 @@ public final class ClusterResolverLoadBalancerProvider extends LoadBalancerProvi // Hostname for resolving endpoints via DNS. Only valid for LOGICAL_DNS clusters. @Nullable final String dnsHostName; + @Nullable + final OutlierDetection outlierDetection; enum Type { EDS, @@ -132,7 +135,8 @@ public final class ClusterResolverLoadBalancerProvider extends LoadBalancerProvi private DiscoveryMechanism(String cluster, Type type, @Nullable String edsServiceName, @Nullable String dnsHostName, @Nullable ServerInfo lrsServerInfo, - @Nullable Long maxConcurrentRequests, @Nullable UpstreamTlsContext tlsContext) { + @Nullable Long maxConcurrentRequests, @Nullable UpstreamTlsContext tlsContext, + @Nullable OutlierDetection outlierDetection) { this.cluster = checkNotNull(cluster, "cluster"); this.type = checkNotNull(type, "type"); this.edsServiceName = edsServiceName; @@ -140,20 +144,22 @@ public final class ClusterResolverLoadBalancerProvider extends LoadBalancerProvi this.lrsServerInfo = lrsServerInfo; this.maxConcurrentRequests = maxConcurrentRequests; this.tlsContext = tlsContext; + this.outlierDetection = outlierDetection; } static DiscoveryMechanism forEds(String cluster, @Nullable String edsServiceName, @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, - @Nullable UpstreamTlsContext tlsContext) { + @Nullable UpstreamTlsContext tlsContext, + OutlierDetection outlierDetection) { return new DiscoveryMechanism(cluster, Type.EDS, edsServiceName, null, lrsServerInfo, - maxConcurrentRequests, tlsContext); + maxConcurrentRequests, tlsContext, outlierDetection); } static DiscoveryMechanism forLogicalDns(String cluster, String dnsHostName, @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, @Nullable UpstreamTlsContext tlsContext) { return new DiscoveryMechanism(cluster, Type.LOGICAL_DNS, null, dnsHostName, - lrsServerInfo, maxConcurrentRequests, tlsContext); + lrsServerInfo, maxConcurrentRequests, tlsContext, null); } @Override diff --git a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java index e53439755b..fad5700f5b 100644 --- a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java +++ b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java @@ -19,6 +19,7 @@ package io.grpc.xds; import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import com.google.protobuf.util.Durations; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext; import io.grpc.Internal; import io.grpc.xds.internal.sds.SslContextProviderSupplier; @@ -254,4 +255,148 @@ public final class EnvoyServerProtoData { defaultFilterChain); } } + + /** + * Corresponds to Envoy proto message {@link + * io.envoyproxy.envoy.config.cluster.v3.OutlierDetection}. Only the fields supported by gRPC are + * included. + * + *

Protobuf Duration fields are represented in their string format (e.g. "10s"). + */ + @AutoValue + abstract static class OutlierDetection { + + @Nullable + abstract Long intervalNanos(); + + @Nullable + abstract Long baseEjectionTimeNanos(); + + @Nullable + abstract Long maxEjectionTimeNanos(); + + @Nullable + abstract Integer maxEjectionPercent(); + + @Nullable + abstract SuccessRateEjection successRateEjection(); + + @Nullable + abstract FailurePercentageEjection failurePercentageEjection(); + + static OutlierDetection create( + @Nullable Long intervalNanos, + @Nullable Long baseEjectionTimeNanos, + @Nullable Long maxEjectionTimeNanos, + @Nullable Integer maxEjectionPercentage, + @Nullable SuccessRateEjection successRateEjection, + @Nullable FailurePercentageEjection failurePercentageEjection) { + return new AutoValue_EnvoyServerProtoData_OutlierDetection(intervalNanos, + baseEjectionTimeNanos, maxEjectionTimeNanos, maxEjectionPercentage, successRateEjection, + failurePercentageEjection); + } + + static OutlierDetection fromEnvoyOutlierDetection( + io.envoyproxy.envoy.config.cluster.v3.OutlierDetection envoyOutlierDetection) { + + Long intervalNanos = envoyOutlierDetection.hasInterval() + ? Durations.toNanos(envoyOutlierDetection.getInterval()) : null; + Long baseEjectionTimeNanos = envoyOutlierDetection.hasBaseEjectionTime() + ? Durations.toNanos(envoyOutlierDetection.getBaseEjectionTime()) : null; + Long maxEjectionTimeNanos = envoyOutlierDetection.hasMaxEjectionTime() + ? Durations.toNanos(envoyOutlierDetection.getMaxEjectionTime()) : null; + Integer maxEjectionPercentage = envoyOutlierDetection.hasMaxEjectionPercent() + ? envoyOutlierDetection.getMaxEjectionPercent().getValue() : null; + + SuccessRateEjection successRateEjection; + // If success rate enforcement has been turned completely off, don't configure this ejection. + if (envoyOutlierDetection.hasEnforcingSuccessRate() + && envoyOutlierDetection.getEnforcingSuccessRate().getValue() == 0) { + successRateEjection = null; + } else { + Integer stdevFactor = envoyOutlierDetection.hasSuccessRateStdevFactor() + ? envoyOutlierDetection.getSuccessRateStdevFactor().getValue() : null; + Integer enforcementPercentage = envoyOutlierDetection.hasEnforcingSuccessRate() + ? envoyOutlierDetection.getEnforcingSuccessRate().getValue() : null; + Integer minimumHosts = envoyOutlierDetection.hasSuccessRateMinimumHosts() + ? envoyOutlierDetection.getSuccessRateMinimumHosts().getValue() : null; + Integer requestVolume = envoyOutlierDetection.hasSuccessRateRequestVolume() + ? envoyOutlierDetection.getSuccessRateMinimumHosts().getValue() : null; + + successRateEjection = SuccessRateEjection.create(stdevFactor, enforcementPercentage, + minimumHosts, requestVolume); + } + + FailurePercentageEjection failurePercentageEjection; + if (envoyOutlierDetection.hasEnforcingFailurePercentage() + && envoyOutlierDetection.getEnforcingFailurePercentage().getValue() == 0) { + failurePercentageEjection = null; + } else { + Integer threshold = envoyOutlierDetection.hasFailurePercentageThreshold() + ? envoyOutlierDetection.getFailurePercentageThreshold().getValue() : null; + Integer enforcementPercentage = envoyOutlierDetection.hasEnforcingFailurePercentage() + ? envoyOutlierDetection.getEnforcingFailurePercentage().getValue() : null; + Integer minimumHosts = envoyOutlierDetection.hasFailurePercentageMinimumHosts() + ? envoyOutlierDetection.getFailurePercentageMinimumHosts().getValue() : null; + Integer requestVolume = envoyOutlierDetection.hasFailurePercentageRequestVolume() + ? envoyOutlierDetection.getFailurePercentageRequestVolume().getValue() : null; + + failurePercentageEjection = FailurePercentageEjection.create(threshold, + enforcementPercentage, minimumHosts, requestVolume); + } + + return create(intervalNanos, baseEjectionTimeNanos, maxEjectionTimeNanos, + maxEjectionPercentage, successRateEjection, failurePercentageEjection); + } + } + + @AutoValue + abstract static class SuccessRateEjection { + + @Nullable + abstract Integer stdevFactor(); + + @Nullable + abstract Integer enforcementPercentage(); + + @Nullable + abstract Integer minimumHosts(); + + @Nullable + abstract Integer requestVolume(); + + static SuccessRateEjection create( + @Nullable Integer stdevFactor, + @Nullable Integer enforcementPercentage, + @Nullable Integer minimumHosts, + @Nullable Integer requestVolume) { + return new AutoValue_EnvoyServerProtoData_SuccessRateEjection(stdevFactor, + enforcementPercentage, minimumHosts, requestVolume); + } + } + + @AutoValue + abstract static class FailurePercentageEjection { + + @Nullable + abstract Integer threshold(); + + @Nullable + abstract Integer enforcementPercentage(); + + @Nullable + abstract Integer minimumHosts(); + + @Nullable + abstract Integer requestVolume(); + + static FailurePercentageEjection create( + @Nullable Integer threshold, + @Nullable Integer enforcementPercentage, + @Nullable Integer minimumHosts, + @Nullable Integer requestVolume) { + return new AutoValue_EnvoyServerProtoData_FailurePercentageEjection(threshold, + enforcementPercentage, minimumHosts, requestVolume); + } + } } diff --git a/xds/src/main/java/io/grpc/xds/XdsClient.java b/xds/src/main/java/io/grpc/xds/XdsClient.java index bdffc36119..028ae155ba 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClient.java +++ b/xds/src/main/java/io/grpc/xds/XdsClient.java @@ -34,6 +34,7 @@ import io.grpc.xds.Bootstrapper.ServerInfo; import io.grpc.xds.Endpoints.DropOverload; import io.grpc.xds.Endpoints.LocalityLbEndpoints; import io.grpc.xds.EnvoyServerProtoData.Listener; +import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.LoadStatsManager2.ClusterDropStats; import io.grpc.xds.LoadStatsManager2.ClusterLocalityStats; @@ -221,6 +222,10 @@ abstract class XdsClient { @Nullable abstract ImmutableList prioritizedClusterNames(); + // Outlier detection configuration. + @Nullable + abstract OutlierDetection outlierDetection(); + static Builder forAggregate(String clusterName, List prioritizedClusterNames) { checkNotNull(prioritizedClusterNames, "prioritizedClusterNames"); return new AutoValue_XdsClient_CdsUpdate.Builder() @@ -234,7 +239,8 @@ abstract class XdsClient { static Builder forEds(String clusterName, @Nullable String edsServiceName, @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, - @Nullable UpstreamTlsContext upstreamTlsContext) { + @Nullable UpstreamTlsContext upstreamTlsContext, + @Nullable OutlierDetection outlierDetection) { return new AutoValue_XdsClient_CdsUpdate.Builder() .clusterName(clusterName) .clusterType(ClusterType.EDS) @@ -244,7 +250,8 @@ abstract class XdsClient { .edsServiceName(edsServiceName) .lrsServerInfo(lrsServerInfo) .maxConcurrentRequests(maxConcurrentRequests) - .upstreamTlsContext(upstreamTlsContext); + .upstreamTlsContext(upstreamTlsContext) + .outlierDetection(outlierDetection); } static Builder forLogicalDns(String clusterName, String dnsHostName, @@ -284,8 +291,9 @@ abstract class XdsClient { .add("dnsHostName", dnsHostName()) .add("lrsServerInfo", lrsServerInfo()) .add("maxConcurrentRequests", maxConcurrentRequests()) - // Exclude upstreamTlsContext as its string representation is cumbersome. .add("prioritizedClusterNames", prioritizedClusterNames()) + // Exclude upstreamTlsContext and outlierDetection as their string representations are + // cumbersome. .toString(); } @@ -341,6 +349,8 @@ abstract class XdsClient { // Private, use CdsUpdate.forAggregate() instead. protected abstract Builder prioritizedClusterNames(List prioritizedClusterNames); + protected abstract Builder outlierDetection(OutlierDetection outlierDetection); + abstract CdsUpdate build(); } } diff --git a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java index a90df303e0..128d7bd395 100644 --- a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java +++ b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java @@ -50,6 +50,8 @@ import io.grpc.xds.Bootstrapper.ServerInfo; import io.grpc.xds.CdsLoadBalancerProvider.CdsConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig.DiscoveryMechanism; +import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; +import io.grpc.xds.EnvoyServerProtoData.SuccessRateEjection; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestConfig; import io.grpc.xds.RingHashLoadBalancer.RingHashConfig; @@ -85,6 +87,9 @@ public class CdsLoadBalancer2Test { ServerInfo.create("lrs.googleapis.com", InsecureChannelCredentials.create(), true); private final UpstreamTlsContext upstreamTlsContext = CommonTlsContextTestsUtil.buildUpstreamTlsContext("google_cloud_private_spiffe", true); + private final OutlierDetection outlierDetection = OutlierDetection.create( + null, null, null, null, SuccessRateEjection.create(null, null, null, null), null); + private final SynchronizationContext syncContext = new SynchronizationContext( new Thread.UncaughtExceptionHandler() { @@ -154,7 +159,8 @@ public class CdsLoadBalancer2Test { @Test public void discoverTopLevelEdsCluster() { CdsUpdate update = - CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext) + CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext, + outlierDetection) .roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(CLUSTER, update); assertThat(childBalancers).hasSize(1); @@ -164,7 +170,7 @@ public class CdsLoadBalancer2Test { assertThat(childLbConfig.discoveryMechanisms).hasSize(1); DiscoveryMechanism instance = Iterables.getOnlyElement(childLbConfig.discoveryMechanisms); assertDiscoveryMechanism(instance, CLUSTER, DiscoveryMechanism.Type.EDS, EDS_SERVICE_NAME, - null, LRS_SERVER_INFO, 100L, upstreamTlsContext); + null, LRS_SERVER_INFO, 100L, upstreamTlsContext, outlierDetection); assertThat(childLbConfig.lbPolicy.getProvider().getPolicyName()).isEqualTo("round_robin"); } @@ -181,7 +187,7 @@ public class CdsLoadBalancer2Test { assertThat(childLbConfig.discoveryMechanisms).hasSize(1); DiscoveryMechanism instance = Iterables.getOnlyElement(childLbConfig.discoveryMechanisms); assertDiscoveryMechanism(instance, CLUSTER, DiscoveryMechanism.Type.LOGICAL_DNS, null, - DNS_HOST_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext); + DNS_HOST_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext, null); assertThat(childLbConfig.lbPolicy.getProvider().getPolicyName()) .isEqualTo("least_request_experimental"); assertThat(((LeastRequestConfig) childLbConfig.lbPolicy.getConfig()).choiceCount).isEqualTo(3); @@ -201,7 +207,7 @@ public class CdsLoadBalancer2Test { @Test public void nonAggregateCluster_resourceUpdate() { CdsUpdate update = - CdsUpdate.forEds(CLUSTER, null, null, 100L, upstreamTlsContext) + CdsUpdate.forEds(CLUSTER, null, null, 100L, upstreamTlsContext, outlierDetection) .roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(CLUSTER, update); assertThat(childBalancers).hasSize(1); @@ -209,15 +215,15 @@ public class CdsLoadBalancer2Test { ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; DiscoveryMechanism instance = Iterables.getOnlyElement(childLbConfig.discoveryMechanisms); assertDiscoveryMechanism(instance, CLUSTER, DiscoveryMechanism.Type.EDS, null, null, null, - 100L, upstreamTlsContext); + 100L, upstreamTlsContext, outlierDetection); - update = CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 200L, null) - .roundRobinLbPolicy().build(); + update = CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 200L, null, + outlierDetection).roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(CLUSTER, update); childLbConfig = (ClusterResolverConfig) childBalancer.config; instance = Iterables.getOnlyElement(childLbConfig.discoveryMechanisms); assertDiscoveryMechanism(instance, CLUSTER, DiscoveryMechanism.Type.EDS, EDS_SERVICE_NAME, - null, LRS_SERVER_INFO, 200L, null); + null, LRS_SERVER_INFO, 200L, null, outlierDetection); } @Test @@ -231,7 +237,7 @@ public class CdsLoadBalancer2Test { ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; DiscoveryMechanism instance = Iterables.getOnlyElement(childLbConfig.discoveryMechanisms); assertDiscoveryMechanism(instance, CLUSTER, DiscoveryMechanism.Type.LOGICAL_DNS, null, - DNS_HOST_NAME, null, 100L, upstreamTlsContext); + DNS_HOST_NAME, null, 100L, upstreamTlsContext, null); xdsClient.deliverResourceNotExist(CLUSTER); assertThat(childBalancer.shutdown).isTrue(); @@ -265,9 +271,8 @@ public class CdsLoadBalancer2Test { assertThat(xdsClient.watchers.keySet()).containsExactly( CLUSTER, cluster1, cluster2, cluster3, cluster4); assertThat(childBalancers).isEmpty(); - CdsUpdate update3 = - CdsUpdate.forEds(cluster3, EDS_SERVICE_NAME, LRS_SERVER_INFO, 200L, upstreamTlsContext) - .roundRobinLbPolicy().build(); + CdsUpdate update3 = CdsUpdate.forEds(cluster3, EDS_SERVICE_NAME, LRS_SERVER_INFO, 200L, + upstreamTlsContext, outlierDetection).roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster3, update3); assertThat(childBalancers).isEmpty(); CdsUpdate update2 = @@ -276,7 +281,7 @@ public class CdsLoadBalancer2Test { xdsClient.deliverCdsUpdate(cluster2, update2); assertThat(childBalancers).isEmpty(); CdsUpdate update4 = - CdsUpdate.forEds(cluster4, null, LRS_SERVER_INFO, 300L, null) + CdsUpdate.forEds(cluster4, null, LRS_SERVER_INFO, 300L, null, outlierDetection) .roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster4, update4); assertThat(childBalancers).hasSize(1); // all non-aggregate clusters discovered @@ -286,12 +291,12 @@ public class CdsLoadBalancer2Test { assertThat(childLbConfig.discoveryMechanisms).hasSize(3); // Clusters on higher level has higher priority: [cluster2, cluster3, cluster4] assertDiscoveryMechanism(childLbConfig.discoveryMechanisms.get(0), cluster2, - DiscoveryMechanism.Type.LOGICAL_DNS, null, DNS_HOST_NAME, null, 100L, null); + DiscoveryMechanism.Type.LOGICAL_DNS, null, DNS_HOST_NAME, null, 100L, null, null); assertDiscoveryMechanism(childLbConfig.discoveryMechanisms.get(1), cluster3, DiscoveryMechanism.Type.EDS, EDS_SERVICE_NAME, null, LRS_SERVER_INFO, 200L, - upstreamTlsContext); + upstreamTlsContext, outlierDetection); assertDiscoveryMechanism(childLbConfig.discoveryMechanisms.get(2), cluster4, - DiscoveryMechanism.Type.EDS, null, null, LRS_SERVER_INFO, 300L, null); + DiscoveryMechanism.Type.EDS, null, null, LRS_SERVER_INFO, 300L, null, outlierDetection); assertThat(childLbConfig.lbPolicy.getProvider().getPolicyName()) .isEqualTo("ring_hash_experimental"); // dominated by top-level cluster's config assertThat(((RingHashConfig) childLbConfig.lbPolicy.getConfig()).minRingSize).isEqualTo(100L); @@ -326,9 +331,8 @@ public class CdsLoadBalancer2Test { .roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(CLUSTER, update); assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1, cluster2); - CdsUpdate update1 = - CdsUpdate.forEds(cluster1, EDS_SERVICE_NAME, LRS_SERVER_INFO, 200L, upstreamTlsContext) - .roundRobinLbPolicy().build(); + CdsUpdate update1 = CdsUpdate.forEds(cluster1, EDS_SERVICE_NAME, LRS_SERVER_INFO, 200L, + upstreamTlsContext, outlierDetection).roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster1, update1); CdsUpdate update2 = CdsUpdate.forLogicalDns(cluster2, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, null) @@ -339,9 +343,10 @@ public class CdsLoadBalancer2Test { assertThat(childLbConfig.discoveryMechanisms).hasSize(2); assertDiscoveryMechanism(childLbConfig.discoveryMechanisms.get(0), cluster1, DiscoveryMechanism.Type.EDS, EDS_SERVICE_NAME, null, LRS_SERVER_INFO, 200L, - upstreamTlsContext); + upstreamTlsContext, outlierDetection); assertDiscoveryMechanism(childLbConfig.discoveryMechanisms.get(1), cluster2, - DiscoveryMechanism.Type.LOGICAL_DNS, null, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, null); + DiscoveryMechanism.Type.LOGICAL_DNS, null, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, null, + null); // Revoke cluster1, should still be able to proceed with cluster2. xdsClient.deliverResourceNotExist(cluster1); @@ -349,7 +354,8 @@ public class CdsLoadBalancer2Test { childLbConfig = (ClusterResolverConfig) childBalancer.config; assertThat(childLbConfig.discoveryMechanisms).hasSize(1); assertDiscoveryMechanism(Iterables.getOnlyElement(childLbConfig.discoveryMechanisms), cluster2, - DiscoveryMechanism.Type.LOGICAL_DNS, null, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, null); + DiscoveryMechanism.Type.LOGICAL_DNS, null, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, null, + null); verify(helper, never()).updateBalancingState( eq(ConnectivityState.TRANSIENT_FAILURE), any(SubchannelPicker.class)); @@ -374,9 +380,8 @@ public class CdsLoadBalancer2Test { .roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(CLUSTER, update); assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster1, cluster2); - CdsUpdate update1 = - CdsUpdate.forEds(cluster1, EDS_SERVICE_NAME, LRS_SERVER_INFO, 200L, upstreamTlsContext) - .roundRobinLbPolicy().build(); + CdsUpdate update1 = CdsUpdate.forEds(cluster1, EDS_SERVICE_NAME, LRS_SERVER_INFO, 200L, + upstreamTlsContext, outlierDetection).roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster1, update1); CdsUpdate update2 = CdsUpdate.forLogicalDns(cluster2, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, null) @@ -387,9 +392,10 @@ public class CdsLoadBalancer2Test { assertThat(childLbConfig.discoveryMechanisms).hasSize(2); assertDiscoveryMechanism(childLbConfig.discoveryMechanisms.get(0), cluster1, DiscoveryMechanism.Type.EDS, EDS_SERVICE_NAME, null, LRS_SERVER_INFO, 200L, - upstreamTlsContext); + upstreamTlsContext, outlierDetection); assertDiscoveryMechanism(childLbConfig.discoveryMechanisms.get(1), cluster2, - DiscoveryMechanism.Type.LOGICAL_DNS, null, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, null); + DiscoveryMechanism.Type.LOGICAL_DNS, null, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, null, + null); xdsClient.deliverResourceNotExist(CLUSTER); assertThat(xdsClient.watchers.keySet()) @@ -428,16 +434,15 @@ public class CdsLoadBalancer2Test { .roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster2, update2); assertThat(xdsClient.watchers.keySet()).containsExactly(CLUSTER, cluster2, cluster3); - CdsUpdate update3 = - CdsUpdate.forEds(cluster3, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext) - .roundRobinLbPolicy().build(); + CdsUpdate update3 = CdsUpdate.forEds(cluster3, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, + upstreamTlsContext, outlierDetection).roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(cluster3, update3); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); ClusterResolverConfig childLbConfig = (ClusterResolverConfig) childBalancer.config; assertThat(childLbConfig.discoveryMechanisms).hasSize(1); DiscoveryMechanism instance = Iterables.getOnlyElement(childLbConfig.discoveryMechanisms); assertDiscoveryMechanism(instance, cluster3, DiscoveryMechanism.Type.EDS, EDS_SERVICE_NAME, - null, LRS_SERVER_INFO, 100L, upstreamTlsContext); + null, LRS_SERVER_INFO, 100L, upstreamTlsContext, outlierDetection); // cluster2 revoked xdsClient.deliverResourceNotExist(cluster2); @@ -506,9 +511,8 @@ public class CdsLoadBalancer2Test { @Test public void handleNameResolutionErrorFromUpstream_afterChildLbCreated_fallThrough() { - CdsUpdate update = - CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext) - .roundRobinLbPolicy().build(); + CdsUpdate update = CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, + upstreamTlsContext, outlierDetection).roundRobinLbPolicy().build(); xdsClient.deliverCdsUpdate(CLUSTER, update); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); assertThat(childBalancer.shutdown).isFalse(); @@ -523,7 +527,8 @@ public class CdsLoadBalancer2Test { public void unknownLbProvider() { try { xdsClient.deliverCdsUpdate(CLUSTER, - CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext) + CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext, + outlierDetection) .lbPolicyConfig(ImmutableMap.of("unknown", ImmutableMap.of("foo", "bar"))).build()); } catch (Exception e) { assertThat(e).hasCauseThat().hasMessageThat().contains("No provider available"); @@ -536,8 +541,8 @@ public class CdsLoadBalancer2Test { public void invalidLbConfig() { try { xdsClient.deliverCdsUpdate(CLUSTER, - CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext) - .lbPolicyConfig( + CdsUpdate.forEds(CLUSTER, EDS_SERVICE_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext, + outlierDetection).lbPolicyConfig( ImmutableMap.of("ring_hash_experimental", ImmutableMap.of("minRingSize", "-1"))) .build()); } catch (Exception e) { @@ -561,7 +566,7 @@ public class CdsLoadBalancer2Test { private static void assertDiscoveryMechanism(DiscoveryMechanism instance, String name, DiscoveryMechanism.Type type, @Nullable String edsServiceName, @Nullable String dnsHostName, @Nullable ServerInfo lrsServerInfo, @Nullable Long maxConcurrentRequests, - @Nullable UpstreamTlsContext tlsContext) { + @Nullable UpstreamTlsContext tlsContext, @Nullable OutlierDetection outlierDetection) { assertThat(instance.cluster).isEqualTo(name); assertThat(instance.type).isEqualTo(type); assertThat(instance.edsServiceName).isEqualTo(edsServiceName); @@ -569,6 +574,7 @@ public class CdsLoadBalancer2Test { assertThat(instance.lrsServerInfo).isEqualTo(lrsServerInfo); assertThat(instance.maxConcurrentRequests).isEqualTo(maxConcurrentRequests); assertThat(instance.tlsContext).isEqualTo(tlsContext); + assertThat(instance.outlierDetection).isEqualTo(outlierDetection); } private final class FakeLoadBalancerProvider extends LoadBalancerProvider { diff --git a/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java b/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java index 53b4027768..97f892a0c2 100644 --- a/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java +++ b/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java @@ -37,8 +37,12 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.protobuf.Any; +import com.google.protobuf.Duration; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; +import com.google.protobuf.UInt32Value; +import com.google.protobuf.util.Durations; +import io.envoyproxy.envoy.config.cluster.v3.OutlierDetection; import io.envoyproxy.envoy.config.route.v3.FilterConfig; import io.envoyproxy.envoy.extensions.filters.http.router.v3.Router; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CertificateProviderPluginInstance; @@ -66,12 +70,15 @@ import io.grpc.xds.AbstractXdsClient.ResourceType; import io.grpc.xds.Bootstrapper.AuthorityInfo; import io.grpc.xds.Bootstrapper.CertificateProviderInfo; import io.grpc.xds.Bootstrapper.ServerInfo; +import io.grpc.xds.ClientXdsClient.ResourceInvalidException; import io.grpc.xds.ClientXdsClient.XdsChannelFactory; 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.FailurePercentageEjection; import io.grpc.xds.EnvoyServerProtoData.FilterChain; +import io.grpc.xds.EnvoyServerProtoData.SuccessRateEjection; import io.grpc.xds.FaultConfig.FractionalPercent.DenominatorType; import io.grpc.xds.LoadStatsManager2.ClusterDropStats; import io.grpc.xds.XdsClient.CdsResourceWatcher; @@ -211,7 +218,7 @@ public abstract class ClientXdsClientTestBase { // CDS test resources. private final Any testClusterRoundRobin = Any.pack(mf.buildEdsCluster(CDS_RESOURCE, null, "round_robin", null, - null, false, null, "envoy.transport_sockets.tls", null + null, false, null, "envoy.transport_sockets.tls", null, null )); // EDS test resources. @@ -1021,7 +1028,7 @@ public abstract class ClientXdsClientTestBase { "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/cluster1"; Any testClusterConfig = Any.pack(mf.buildEdsCluster( cdsResourceNameWithWrongType, null, "round_robin", null, null, false, null, - "envoy.transport_sockets.tls", null)); + "envoy.transport_sockets.tls", null, null)); call.sendResponse(CDS, testClusterConfig, VERSION_1, "0000"); call.verifyRequestNack( CDS, cdsResourceName, "", "0000", NODE, @@ -1615,9 +1622,9 @@ public abstract class ClientXdsClientTestBase { List clusters = ImmutableList.of( Any.pack(mf.buildEdsCluster("cluster-bar.googleapis.com", null, "round_robin", null, - null, false, null, "envoy.transport_sockets.tls", null)), + null, false, null, "envoy.transport_sockets.tls", null, null)), Any.pack(mf.buildEdsCluster("cluster-baz.googleapis.com", null, "round_robin", null, - null, false, null, "envoy.transport_sockets.tls", null))); + null, false, null, "envoy.transport_sockets.tls", null, null))); call.sendResponse(CDS, clusters, VERSION_1, "0000"); // Client sent an ACK CDS request. @@ -1692,13 +1699,13 @@ public abstract class ClientXdsClientTestBase { // CDS -> {A, B, C}, version 1 ImmutableMap resourcesV1 = ImmutableMap.of( "A", Any.pack(mf.buildEdsCluster("A", "A.1", "round_robin", null, null, false, null, - "envoy.transport_sockets.tls", null + "envoy.transport_sockets.tls", null, null )), "B", Any.pack(mf.buildEdsCluster("B", "B.1", "round_robin", null, null, false, null, - "envoy.transport_sockets.tls", null + "envoy.transport_sockets.tls", null, null )), "C", Any.pack(mf.buildEdsCluster("C", "C.1", "round_robin", null, null, false, null, - "envoy.transport_sockets.tls", null + "envoy.transport_sockets.tls", null, null ))); call.sendResponse(CDS, resourcesV1.values().asList(), VERSION_1, "0000"); // {A, B, C} -> ACK, version 1 @@ -1711,7 +1718,7 @@ public abstract class ClientXdsClientTestBase { // Failed to parse endpoint B ImmutableMap resourcesV2 = ImmutableMap.of( "A", Any.pack(mf.buildEdsCluster("A", "A.2", "round_robin", null, null, false, null, - "envoy.transport_sockets.tls", null + "envoy.transport_sockets.tls", null, null )), "B", Any.pack(mf.buildClusterInvalid("B"))); call.sendResponse(CDS, resourcesV2.values().asList(), VERSION_2, "0001"); @@ -1733,10 +1740,10 @@ public abstract class ClientXdsClientTestBase { // CDS -> {B, C} version 3 ImmutableMap resourcesV3 = ImmutableMap.of( "B", Any.pack(mf.buildEdsCluster("B", "B.3", "round_robin", null, null, false, null, - "envoy.transport_sockets.tls", null + "envoy.transport_sockets.tls", null, null )), "C", Any.pack(mf.buildEdsCluster("C", "C.3", "round_robin", null, null, false, null, - "envoy.transport_sockets.tls", null + "envoy.transport_sockets.tls", null, null ))); call.sendResponse(CDS, resourcesV3.values().asList(), VERSION_3, "0002"); // {A} -> does not exit @@ -1774,13 +1781,13 @@ public abstract class ClientXdsClientTestBase { // CDS -> {A, B, C}, version 1 ImmutableMap resourcesV1 = ImmutableMap.of( "A", Any.pack(mf.buildEdsCluster("A", "A.1", "round_robin", null, null, false, null, - "envoy.transport_sockets.tls", null + "envoy.transport_sockets.tls", null, null )), "B", Any.pack(mf.buildEdsCluster("B", "B.1", "round_robin", null, null, false, null, - "envoy.transport_sockets.tls", null + "envoy.transport_sockets.tls", null, null )), "C", Any.pack(mf.buildEdsCluster("C", "C.1", "round_robin", null, null, false, null, - "envoy.transport_sockets.tls", null + "envoy.transport_sockets.tls", null, null ))); call.sendResponse(CDS, resourcesV1.values().asList(), VERSION_1, "0000"); // {A, B, C} -> ACK, version 1 @@ -1806,7 +1813,7 @@ public abstract class ClientXdsClientTestBase { // Failed to parse endpoint B ImmutableMap resourcesV2 = ImmutableMap.of( "A", Any.pack(mf.buildEdsCluster("A", "A.2", "round_robin", null, null, false, null, - "envoy.transport_sockets.tls", null + "envoy.transport_sockets.tls", null, null )), "B", Any.pack(mf.buildClusterInvalid("B"))); call.sendResponse(CDS, resourcesV2.values().asList(), VERSION_2, "0001"); @@ -1876,7 +1883,7 @@ public abstract class ClientXdsClientTestBase { Message leastRequestConfig = mf.buildLeastRequestLbConfig(3); Any clusterRingHash = Any.pack( mf.buildEdsCluster(CDS_RESOURCE, null, "least_request_experimental", null, - leastRequestConfig, false, null, "envoy.transport_sockets.tls", null + leastRequestConfig, false, null, "envoy.transport_sockets.tls", null, null )); call.sendResponse(ResourceType.CDS, clusterRingHash, VERSION_1, "0000"); @@ -1907,7 +1914,7 @@ public abstract class ClientXdsClientTestBase { Message ringHashConfig = mf.buildRingHashLbConfig("xx_hash", 10L, 100L); Any clusterRingHash = Any.pack( mf.buildEdsCluster(CDS_RESOURCE, null, "ring_hash_experimental", ringHashConfig, null, - false, null, "envoy.transport_sockets.tls", null + false, null, "envoy.transport_sockets.tls", null, null )); call.sendResponse(ResourceType.CDS, clusterRingHash, VERSION_1, "0000"); @@ -1962,7 +1969,7 @@ public abstract class ClientXdsClientTestBase { DiscoveryRpcCall call = startResourceWatcher(CDS, CDS_RESOURCE, cdsResourceWatcher); Any clusterCircuitBreakers = Any.pack( mf.buildEdsCluster(CDS_RESOURCE, null, "round_robin", null, null, false, null, - "envoy.transport_sockets.tls", mf.buildCircuitBreakers(50, 200))); + "envoy.transport_sockets.tls", mf.buildCircuitBreakers(50, 200), null)); call.sendResponse(CDS, clusterCircuitBreakers, VERSION_1, "0000"); // Client sent an ACK CDS request. @@ -1999,13 +2006,13 @@ public abstract class ClientXdsClientTestBase { Any.pack(mf.buildEdsCluster(CDS_RESOURCE, "eds-cluster-foo.googleapis.com", "round_robin", null, null, true, mf.buildUpstreamTlsContext("cert-instance-name", "cert1"), - "envoy.transport_sockets.tls", null)); + "envoy.transport_sockets.tls", null, null)); List clusters = ImmutableList.of( Any.pack(mf.buildLogicalDnsCluster("cluster-bar.googleapis.com", "dns-service-bar.googleapis.com", 443, "round_robin", null, null,false, null, null)), clusterEds, Any.pack(mf.buildEdsCluster("cluster-baz.googleapis.com", null, "round_robin", null, null, - false, null, "envoy.transport_sockets.tls", null))); + false, null, "envoy.transport_sockets.tls", null, null))); call.sendResponse(CDS, clusters, VERSION_1, "0000"); // Client sent an ACK CDS request. @@ -2035,13 +2042,13 @@ public abstract class ClientXdsClientTestBase { Any.pack(mf.buildEdsCluster(CDS_RESOURCE, "eds-cluster-foo.googleapis.com", "round_robin", null, null,true, mf.buildNewUpstreamTlsContext("cert-instance-name", "cert1"), - "envoy.transport_sockets.tls", null)); + "envoy.transport_sockets.tls", null, null)); List clusters = ImmutableList.of( Any.pack(mf.buildLogicalDnsCluster("cluster-bar.googleapis.com", "dns-service-bar.googleapis.com", 443, "round_robin", null, null, false, null, null)), clusterEds, Any.pack(mf.buildEdsCluster("cluster-baz.googleapis.com", null, "round_robin", null, null, - false, null, "envoy.transport_sockets.tls", null))); + false, null, "envoy.transport_sockets.tls", null, null))); call.sendResponse(CDS, clusters, VERSION_1, "0000"); // Client sent an ACK CDS request. @@ -2069,7 +2076,7 @@ public abstract class ClientXdsClientTestBase { List clusters = ImmutableList.of(Any .pack(mf.buildEdsCluster(CDS_RESOURCE, "eds-cluster-foo.googleapis.com", "round_robin", null, null, true, - mf.buildUpstreamTlsContext(null, null), "envoy.transport_sockets.tls", null))); + mf.buildUpstreamTlsContext(null, null), "envoy.transport_sockets.tls", null, null))); call.sendResponse(CDS, clusters, VERSION_1, "0000"); // The response NACKed with errors indicating indices of the failed resources. @@ -2082,6 +2089,224 @@ public abstract class ClientXdsClientTestBase { verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); } + /** + * CDS response containing OutlierDetection for a cluster. + */ + @Test + @SuppressWarnings("deprecation") + public void cdsResponseWithOutlierDetection() { + Assume.assumeTrue(useProtocolV3()); + ClientXdsClient.enableOutlierDetection = true; + + DiscoveryRpcCall call = startResourceWatcher(CDS, CDS_RESOURCE, cdsResourceWatcher); + + OutlierDetection outlierDetectionXds = OutlierDetection.newBuilder() + .setInterval(Durations.fromNanos(100)) + .setBaseEjectionTime(Durations.fromNanos(100)) + .setMaxEjectionTime(Durations.fromNanos(100)) + .setMaxEjectionPercent(UInt32Value.of(100)) + .setSuccessRateStdevFactor(UInt32Value.of(100)) + .setEnforcingSuccessRate(UInt32Value.of(100)) + .setSuccessRateMinimumHosts(UInt32Value.of(100)) + .setSuccessRateRequestVolume(UInt32Value.of(100)) + .setFailurePercentageThreshold(UInt32Value.of(100)) + .setEnforcingFailurePercentage(UInt32Value.of(100)) + .setFailurePercentageMinimumHosts(UInt32Value.of(100)) + .setFailurePercentageRequestVolume(UInt32Value.of(100)).build(); + + // Management server sends back CDS response with UpstreamTlsContext. + Any clusterEds = + Any.pack(mf.buildEdsCluster(CDS_RESOURCE, "eds-cluster-foo.googleapis.com", "round_robin", + null, null, true, + mf.buildUpstreamTlsContext("cert-instance-name", "cert1"), + "envoy.transport_sockets.tls", null, outlierDetectionXds)); + List clusters = ImmutableList.of( + Any.pack(mf.buildLogicalDnsCluster("cluster-bar.googleapis.com", + "dns-service-bar.googleapis.com", 443, "round_robin", null, null,false, null, null)), + clusterEds, + Any.pack(mf.buildEdsCluster("cluster-baz.googleapis.com", null, "round_robin", null, null, + false, null, "envoy.transport_sockets.tls", null, outlierDetectionXds))); + call.sendResponse(CDS, clusters, VERSION_1, "0000"); + + // Client sent an ACK CDS request. + call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); + verify(cdsResourceWatcher, times(1)).onChanged(cdsUpdateCaptor.capture()); + CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); + + // The outlier detection config in CdsUpdate should match what we get from xDS. + EnvoyServerProtoData.OutlierDetection outlierDetection = cdsUpdate.outlierDetection(); + assertThat(outlierDetection).isNotNull(); + assertThat(outlierDetection.intervalNanos()).isEqualTo(100); + assertThat(outlierDetection.baseEjectionTimeNanos()).isEqualTo(100); + assertThat(outlierDetection.maxEjectionTimeNanos()).isEqualTo(100); + assertThat(outlierDetection.maxEjectionPercent()).isEqualTo(100); + + SuccessRateEjection successRateEjection = outlierDetection.successRateEjection(); + assertThat(successRateEjection).isNotNull(); + assertThat(successRateEjection.stdevFactor()).isEqualTo(100); + assertThat(successRateEjection.enforcementPercentage()).isEqualTo(100); + assertThat(successRateEjection.minimumHosts()).isEqualTo(100); + assertThat(successRateEjection.requestVolume()).isEqualTo(100); + + FailurePercentageEjection failurePercentageEjection + = outlierDetection.failurePercentageEjection(); + assertThat(failurePercentageEjection).isNotNull(); + assertThat(failurePercentageEjection.threshold()).isEqualTo(100); + assertThat(failurePercentageEjection.enforcementPercentage()).isEqualTo(100); + assertThat(failurePercentageEjection.minimumHosts()).isEqualTo(100); + assertThat(failurePercentageEjection.requestVolume()).isEqualTo(100); + + verifyResourceMetadataAcked(CDS, CDS_RESOURCE, clusterEds, VERSION_1, TIME_INCREMENT); + verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); + } + + /** + * CDS response containing OutlierDetection for a cluster, but support has not been enabled. + */ + @Test + @SuppressWarnings("deprecation") + public void cdsResponseWithOutlierDetection_supportDisabled() { + Assume.assumeTrue(useProtocolV3()); + ClientXdsClient.enableOutlierDetection = false; + + DiscoveryRpcCall call = startResourceWatcher(CDS, CDS_RESOURCE, cdsResourceWatcher); + + OutlierDetection outlierDetectionXds = OutlierDetection.newBuilder() + .setInterval(Durations.fromNanos(100)).build(); + + // Management server sends back CDS response with UpstreamTlsContext. + Any clusterEds = + Any.pack(mf.buildEdsCluster(CDS_RESOURCE, "eds-cluster-foo.googleapis.com", "round_robin", + null, null, true, + mf.buildUpstreamTlsContext("cert-instance-name", "cert1"), + "envoy.transport_sockets.tls", null, outlierDetectionXds)); + List clusters = ImmutableList.of( + Any.pack(mf.buildLogicalDnsCluster("cluster-bar.googleapis.com", + "dns-service-bar.googleapis.com", 443, "round_robin", null, null,false, null, null)), + clusterEds, + Any.pack(mf.buildEdsCluster("cluster-baz.googleapis.com", null, "round_robin", null, null, + false, null, "envoy.transport_sockets.tls", null, outlierDetectionXds))); + call.sendResponse(CDS, clusters, VERSION_1, "0000"); + + // Client sent an ACK CDS request. + call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); + verify(cdsResourceWatcher, times(1)).onChanged(cdsUpdateCaptor.capture()); + CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); + + assertThat(cdsUpdate.outlierDetection()).isNull(); + + verifyResourceMetadataAcked(CDS, CDS_RESOURCE, clusterEds, VERSION_1, TIME_INCREMENT); + verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); + } + + /** + * CDS response containing OutlierDetection for a cluster. + */ + @Test + @SuppressWarnings("deprecation") + public void cdsResponseWithInvalidOutlierDetectionNacks() { + Assume.assumeTrue(useProtocolV3()); + ClientXdsClient.enableOutlierDetection = true; + + DiscoveryRpcCall call = startResourceWatcher(CDS, CDS_RESOURCE, cdsResourceWatcher); + + OutlierDetection outlierDetectionXds = OutlierDetection.newBuilder() + .setMaxEjectionPercent(UInt32Value.of(101)).build(); + + // Management server sends back CDS response with UpstreamTlsContext. + Any clusterEds = + Any.pack(mf.buildEdsCluster(CDS_RESOURCE, "eds-cluster-foo.googleapis.com", "round_robin", + null, null, true, + mf.buildUpstreamTlsContext("cert-instance-name", "cert1"), + "envoy.transport_sockets.tls", null, outlierDetectionXds)); + List clusters = ImmutableList.of( + Any.pack(mf.buildLogicalDnsCluster("cluster-bar.googleapis.com", + "dns-service-bar.googleapis.com", 443, "round_robin", null, null,false, null, null)), + clusterEds, + Any.pack(mf.buildEdsCluster("cluster-baz.googleapis.com", null, "round_robin", null, null, + false, null, "envoy.transport_sockets.tls", null, outlierDetectionXds))); + call.sendResponse(CDS, clusters, VERSION_1, "0000"); + + String errorMsg = "CDS response Cluster 'cluster.googleapis.com' validation error: " + + "Cluster cluster.googleapis.com: malformed outlier_detection: " + + "io.grpc.xds.ClientXdsClient$ResourceInvalidException: outlier_detection " + + "max_ejection_percent is > 100"; + call.verifyRequestNack(CDS, CDS_RESOURCE, "", "0000", NODE, ImmutableList.of(errorMsg)); + verify(cdsResourceWatcher).onError(errorCaptor.capture()); + verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); + } + + @Test(expected = ResourceInvalidException.class) + public void validateOutlierDetection_invalidInterval() throws ResourceInvalidException { + ClientXdsClient.validateOutlierDetection( + OutlierDetection.newBuilder().setInterval(Duration.newBuilder().setSeconds(Long.MAX_VALUE)) + .build()); + } + + @Test(expected = ResourceInvalidException.class) + public void validateOutlierDetection_negativeInterval() throws ResourceInvalidException { + ClientXdsClient.validateOutlierDetection( + OutlierDetection.newBuilder().setInterval(Duration.newBuilder().setSeconds(-1)) + .build()); + } + + @Test(expected = ResourceInvalidException.class) + public void validateOutlierDetection_invalidBaseEjectionTime() throws ResourceInvalidException { + ClientXdsClient.validateOutlierDetection( + OutlierDetection.newBuilder() + .setBaseEjectionTime(Duration.newBuilder().setSeconds(Long.MAX_VALUE)) + .build()); + } + + @Test(expected = ResourceInvalidException.class) + public void validateOutlierDetection_negativeBaseEjectionTime() throws ResourceInvalidException { + ClientXdsClient.validateOutlierDetection( + OutlierDetection.newBuilder().setBaseEjectionTime(Duration.newBuilder().setSeconds(-1)) + .build()); + } + + @Test(expected = ResourceInvalidException.class) + public void validateOutlierDetection_invalidMaxEjectionTime() throws ResourceInvalidException { + ClientXdsClient.validateOutlierDetection( + OutlierDetection.newBuilder() + .setMaxEjectionTime(Duration.newBuilder().setSeconds(Long.MAX_VALUE)) + .build()); + } + + @Test(expected = ResourceInvalidException.class) + public void validateOutlierDetection_negativeMaxEjectionTime() throws ResourceInvalidException { + ClientXdsClient.validateOutlierDetection( + OutlierDetection.newBuilder().setMaxEjectionTime(Duration.newBuilder().setSeconds(-1)) + .build()); + } + + @Test(expected = ResourceInvalidException.class) + public void validateOutlierDetection_maxEjectionPercentTooHigh() throws ResourceInvalidException { + ClientXdsClient.validateOutlierDetection( + OutlierDetection.newBuilder().setMaxEjectionPercent(UInt32Value.of(101)).build()); + } + + @Test(expected = ResourceInvalidException.class) + public void validateOutlierDetection_enforcingSuccessRateTooHigh() + throws ResourceInvalidException { + ClientXdsClient.validateOutlierDetection( + OutlierDetection.newBuilder().setEnforcingSuccessRate(UInt32Value.of(101)).build()); + } + + @Test(expected = ResourceInvalidException.class) + public void validateOutlierDetection_failurePercentageThresholdTooHigh() + throws ResourceInvalidException { + ClientXdsClient.validateOutlierDetection( + OutlierDetection.newBuilder().setFailurePercentageThreshold(UInt32Value.of(101)).build()); + } + + @Test(expected = ResourceInvalidException.class) + public void validateOutlierDetection_enforcingFailurePercentageTooHigh() + throws ResourceInvalidException { + ClientXdsClient.validateOutlierDetection( + OutlierDetection.newBuilder().setEnforcingFailurePercentage(UInt32Value.of(101)).build()); + } + /** * CDS response containing UpstreamTlsContext with bad transportSocketName for a cluster. */ @@ -2094,7 +2319,8 @@ public abstract class ClientXdsClientTestBase { List clusters = ImmutableList.of(Any .pack(mf.buildEdsCluster(CDS_RESOURCE, "eds-cluster-foo.googleapis.com", "round_robin", null, null, true, - mf.buildUpstreamTlsContext("secret1", "cert1"), "envoy.transport_sockets.bad", null))); + mf.buildUpstreamTlsContext("secret1", "cert1"), "envoy.transport_sockets.bad", null, + null))); call.sendResponse(CDS, clusters, VERSION_1, "0000"); // The response NACKed with errors indicating indices of the failed resources. @@ -2169,7 +2395,7 @@ public abstract class ClientXdsClientTestBase { String edsService = "eds-service-bar.googleapis.com"; Any clusterEds = Any.pack( mf.buildEdsCluster(CDS_RESOURCE, edsService, "round_robin", null, null, true, null, - "envoy.transport_sockets.tls", null + "envoy.transport_sockets.tls", null, null )); call.sendResponse(CDS, clusterEds, VERSION_2, "0001"); call.verifyRequest(CDS, CDS_RESOURCE, VERSION_2, "0001", NODE); @@ -2199,17 +2425,17 @@ public abstract class ClientXdsClientTestBase { String transportSocketName = "envoy.transport_sockets.tls"; Any roundRobinConfig = Any.pack( mf.buildEdsCluster(CDS_RESOURCE, edsService, "round_robin", null, null, true, null, - transportSocketName, null + transportSocketName, null, null )); Any ringHashConfig = Any.pack( mf.buildEdsCluster(CDS_RESOURCE, edsService, "ring_hash_experimental", mf.buildRingHashLbConfig("xx_hash", 1, 2), null, true, null, - transportSocketName, null + transportSocketName, null, null )); Any leastRequestConfig = Any.pack( mf.buildEdsCluster(CDS_RESOURCE, edsService, "least_request_experimental", null, mf.buildLeastRequestLbConfig(2), true, null, - transportSocketName, null + transportSocketName, null, null )); // Configure with round robin, the update should be sent to the watcher. @@ -2332,7 +2558,7 @@ public abstract class ClientXdsClientTestBase { Any.pack(mf.buildLogicalDnsCluster(CDS_RESOURCE, dnsHostAddr, dnsHostPort, "round_robin", null, null, false, null, null)), Any.pack(mf.buildEdsCluster(cdsResourceTwo, edsService, "round_robin", null, null, true, - null, "envoy.transport_sockets.tls", null))); + null, "envoy.transport_sockets.tls", null, null))); call.sendResponse(CDS, clusters, VERSION_1, "0000"); verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); @@ -2643,10 +2869,10 @@ public abstract class ClientXdsClientTestBase { DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); List clusters = ImmutableList.of( Any.pack(mf.buildEdsCluster(resource, null, "round_robin", null, null, true, null, - "envoy.transport_sockets.tls", null + "envoy.transport_sockets.tls", null, null )), Any.pack(mf.buildEdsCluster(CDS_RESOURCE, EDS_RESOURCE, "round_robin", null, null, false, - null, "envoy.transport_sockets.tls", null))); + null, "envoy.transport_sockets.tls", null, null))); call.sendResponse(CDS, clusters, VERSION_1, "0000"); verify(cdsWatcher).onChanged(cdsUpdateCaptor.capture()); CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); @@ -2692,9 +2918,9 @@ public abstract class ClientXdsClientTestBase { clusters = ImmutableList.of( Any.pack(mf.buildEdsCluster(resource, null, "round_robin", null, null, true, null, - "envoy.transport_sockets.tls", null)), // no change + "envoy.transport_sockets.tls", null, null)), // no change Any.pack(mf.buildEdsCluster(CDS_RESOURCE, null, "round_robin", null, null, false, null, - "envoy.transport_sockets.tls", null + "envoy.transport_sockets.tls", null, null ))); call.sendResponse(CDS, clusters, VERSION_2, "0001"); verify(cdsResourceWatcher, times(2)).onChanged(cdsUpdateCaptor.capture()); @@ -3256,7 +3482,7 @@ public abstract class ClientXdsClientTestBase { protected abstract Message buildEdsCluster(String clusterName, @Nullable String edsServiceName, String lbPolicy, @Nullable Message ringHashLbConfig, @Nullable Message leastRequestLbConfig, boolean enableLrs, @Nullable Message upstreamTlsContext, String transportSocketName, - @Nullable Message circuitBreakers); + @Nullable Message circuitBreakers, @Nullable Message outlierDetection); protected abstract Message buildLogicalDnsCluster(String clusterName, String dnsHostAddr, int dnsHostPort, String lbPolicy, @Nullable Message ringHashLbConfig, diff --git a/xds/src/test/java/io/grpc/xds/ClientXdsClientV2Test.java b/xds/src/test/java/io/grpc/xds/ClientXdsClientV2Test.java index b302b56dbb..3e2cafd58f 100644 --- a/xds/src/test/java/io/grpc/xds/ClientXdsClientV2Test.java +++ b/xds/src/test/java/io/grpc/xds/ClientXdsClientV2Test.java @@ -52,6 +52,7 @@ import io.envoyproxy.envoy.api.v2.auth.SdsSecretConfig; import io.envoyproxy.envoy.api.v2.auth.UpstreamTlsContext; import io.envoyproxy.envoy.api.v2.cluster.CircuitBreakers; import io.envoyproxy.envoy.api.v2.cluster.CircuitBreakers.Thresholds; +import io.envoyproxy.envoy.api.v2.cluster.OutlierDetection; import io.envoyproxy.envoy.api.v2.core.Address; import io.envoyproxy.envoy.api.v2.core.AggregatedConfigSource; import io.envoyproxy.envoy.api.v2.core.ApiConfigSource; @@ -420,10 +421,10 @@ public class ClientXdsClientV2Test extends ClientXdsClientTestBase { String lbPolicy, @Nullable Message ringHashLbConfig, @Nullable Message leastRequestLbConfig, boolean enableLrs, @Nullable Message upstreamTlsContext, String transportSocketName, - @Nullable Message circuitBreakers) { + @Nullable Message circuitBreakers, @Nullable Message outlierDetection) { Cluster.Builder builder = initClusterBuilder( clusterName, lbPolicy, ringHashLbConfig, leastRequestLbConfig, - enableLrs, upstreamTlsContext, circuitBreakers); + enableLrs, upstreamTlsContext, circuitBreakers, outlierDetection); builder.setType(DiscoveryType.EDS); EdsClusterConfig.Builder edsClusterConfigBuilder = EdsClusterConfig.newBuilder(); edsClusterConfigBuilder.setEdsConfig( @@ -442,7 +443,7 @@ public class ClientXdsClientV2Test extends ClientXdsClientTestBase { @Nullable Message upstreamTlsContext, @Nullable Message circuitBreakers) { Cluster.Builder builder = initClusterBuilder( clusterName, lbPolicy, ringHashLbConfig, leastRequestLbConfig, - enableLrs, upstreamTlsContext, circuitBreakers); + enableLrs, upstreamTlsContext, circuitBreakers, null); builder.setType(DiscoveryType.LOGICAL_DNS); builder.setLoadAssignment( ClusterLoadAssignment.newBuilder().addEndpoints( @@ -483,7 +484,7 @@ public class ClientXdsClientV2Test extends ClientXdsClientTestBase { private Cluster.Builder initClusterBuilder(String clusterName, String lbPolicy, @Nullable Message ringHashLbConfig, @Nullable Message leastRequestLbConfig, boolean enableLrs, @Nullable Message upstreamTlsContext, - @Nullable Message circuitBreakers) { + @Nullable Message circuitBreakers, @Nullable Message outlierDetection) { Cluster.Builder builder = Cluster.newBuilder(); builder.setName(clusterName); if (lbPolicy.equals("round_robin")) { @@ -511,6 +512,9 @@ public class ClientXdsClientV2Test extends ClientXdsClientTestBase { if (circuitBreakers != null) { builder.setCircuitBreakers((CircuitBreakers) circuitBreakers); } + if (outlierDetection != null) { + builder.setOutlierDetection((OutlierDetection) outlierDetection); + } return builder; } diff --git a/xds/src/test/java/io/grpc/xds/ClientXdsClientV3Test.java b/xds/src/test/java/io/grpc/xds/ClientXdsClientV3Test.java index 4f86b178d2..b051643c0b 100644 --- a/xds/src/test/java/io/grpc/xds/ClientXdsClientV3Test.java +++ b/xds/src/test/java/io/grpc/xds/ClientXdsClientV3Test.java @@ -41,6 +41,7 @@ import io.envoyproxy.envoy.config.cluster.v3.Cluster.LbPolicy; import io.envoyproxy.envoy.config.cluster.v3.Cluster.LeastRequestLbConfig; import io.envoyproxy.envoy.config.cluster.v3.Cluster.RingHashLbConfig; import io.envoyproxy.envoy.config.cluster.v3.Cluster.RingHashLbConfig.HashFunction; +import io.envoyproxy.envoy.config.cluster.v3.OutlierDetection; import io.envoyproxy.envoy.config.core.v3.Address; import io.envoyproxy.envoy.config.core.v3.AggregatedConfigSource; import io.envoyproxy.envoy.config.core.v3.ConfigSource; @@ -476,10 +477,10 @@ public class ClientXdsClientV3Test extends ClientXdsClientTestBase { String lbPolicy, @Nullable Message ringHashLbConfig, @Nullable Message leastRequestLbConfig, boolean enableLrs, @Nullable Message upstreamTlsContext, String transportSocketName, - @Nullable Message circuitBreakers) { + @Nullable Message circuitBreakers, @Nullable Message outlierDetection) { Cluster.Builder builder = initClusterBuilder( clusterName, lbPolicy, ringHashLbConfig, leastRequestLbConfig, - enableLrs, upstreamTlsContext, transportSocketName, circuitBreakers); + enableLrs, upstreamTlsContext, transportSocketName, circuitBreakers, outlierDetection); builder.setType(DiscoveryType.EDS); EdsClusterConfig.Builder edsClusterConfigBuilder = EdsClusterConfig.newBuilder(); edsClusterConfigBuilder.setEdsConfig( @@ -498,7 +499,7 @@ public class ClientXdsClientV3Test extends ClientXdsClientTestBase { @Nullable Message upstreamTlsContext, @Nullable Message circuitBreakers) { Cluster.Builder builder = initClusterBuilder( clusterName, lbPolicy, ringHashLbConfig, leastRequestLbConfig, - enableLrs, upstreamTlsContext, "envoy.transport_sockets.tls", circuitBreakers); + enableLrs, upstreamTlsContext, "envoy.transport_sockets.tls", circuitBreakers, null); builder.setType(DiscoveryType.LOGICAL_DNS); builder.setLoadAssignment( ClusterLoadAssignment.newBuilder().addEndpoints( @@ -539,7 +540,7 @@ public class ClientXdsClientV3Test extends ClientXdsClientTestBase { private Cluster.Builder initClusterBuilder(String clusterName, String lbPolicy, @Nullable Message ringHashLbConfig, @Nullable Message leastRequestLbConfig, boolean enableLrs, @Nullable Message upstreamTlsContext, String transportSocketName, - @Nullable Message circuitBreakers) { + @Nullable Message circuitBreakers, @Nullable Message outlierDetection) { Cluster.Builder builder = Cluster.newBuilder(); builder.setName(clusterName); if (lbPolicy.equals("round_robin")) { @@ -567,6 +568,9 @@ public class ClientXdsClientV3Test extends ClientXdsClientTestBase { if (circuitBreakers != null) { builder.setCircuitBreakers((CircuitBreakers) circuitBreakers); } + if (outlierDetection != null) { + builder.setOutlierDetection((OutlierDetection) outlierDetection); + } return builder; } diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java index 22c54bbb94..2a8b95260f 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java @@ -58,6 +58,8 @@ import io.grpc.internal.FakeClock.ScheduledTask; import io.grpc.internal.GrpcUtil; import io.grpc.internal.ObjectPool; import io.grpc.internal.ServiceConfigUtil.PolicySelection; +import io.grpc.util.OutlierDetectionLoadBalancer.OutlierDetectionLoadBalancerConfig; +import io.grpc.util.OutlierDetectionLoadBalancerProvider; import io.grpc.xds.Bootstrapper.ServerInfo; import io.grpc.xds.ClusterImplLoadBalancerProvider.ClusterImplConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig; @@ -65,6 +67,9 @@ import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig.Dis import io.grpc.xds.Endpoints.DropOverload; import io.grpc.xds.Endpoints.LbEndpoint; import io.grpc.xds.Endpoints.LocalityLbEndpoints; +import io.grpc.xds.EnvoyServerProtoData.FailurePercentageEjection; +import io.grpc.xds.EnvoyServerProtoData.OutlierDetection; +import io.grpc.xds.EnvoyServerProtoData.SuccessRateEjection; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestConfig; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig; @@ -116,10 +121,18 @@ public class ClusterResolverLoadBalancerTest { Locality.create("test-region-3", "test-zone-3", "test-subzone-3"); private final UpstreamTlsContext tlsContext = CommonTlsContextTestsUtil.buildUpstreamTlsContext("google_cloud_private_spiffe", true); + private final OutlierDetection outlierDetection = OutlierDetection.create( + 100L, 100L, 100L, 100, SuccessRateEjection.create(100, 100, 100, 100), + FailurePercentageEjection.create(100, 100, 100, 100)); private final DiscoveryMechanism edsDiscoveryMechanism1 = - DiscoveryMechanism.forEds(CLUSTER1, EDS_SERVICE_NAME1, LRS_SERVER_INFO, 100L, tlsContext); + DiscoveryMechanism.forEds(CLUSTER1, EDS_SERVICE_NAME1, LRS_SERVER_INFO, 100L, tlsContext, + null); private final DiscoveryMechanism edsDiscoveryMechanism2 = - DiscoveryMechanism.forEds(CLUSTER2, EDS_SERVICE_NAME2, LRS_SERVER_INFO, 200L, tlsContext); + DiscoveryMechanism.forEds(CLUSTER2, EDS_SERVICE_NAME2, LRS_SERVER_INFO, 200L, tlsContext, + null); + private final DiscoveryMechanism edsDiscoveryMechanismWithOutlierDetection = + DiscoveryMechanism.forEds(CLUSTER1, EDS_SERVICE_NAME1, LRS_SERVER_INFO, 100L, tlsContext, + outlierDetection); private final DiscoveryMechanism logicalDnsDiscoveryMechanism = DiscoveryMechanism.forLogicalDns(CLUSTER_DNS, DNS_HOST_NAME, LRS_SERVER_INFO, 300L, null); @@ -182,6 +195,7 @@ public class ClusterResolverLoadBalancerTest { lbRegistry.register(new FakeLoadBalancerProvider(WEIGHTED_TARGET_POLICY_NAME)); lbRegistry.register( new FakeLoadBalancerProvider("pick_first")); // needed by logical_dns + lbRegistry.register(new OutlierDetectionLoadBalancerProvider()); NameResolver.Args args = NameResolver.Args.newBuilder() .setDefaultPort(8080) .setProxyDetector(GrpcUtil.NOOP_PROXY_DETECTOR) @@ -317,6 +331,90 @@ public class ClusterResolverLoadBalancerTest { locality1, 100); } + @Test + public void edsClustersWithOutlierDetection() { + ClusterResolverConfig config = new ClusterResolverConfig( + Collections.singletonList(edsDiscoveryMechanismWithOutlierDetection), leastRequest); + deliverLbConfig(config); + assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); + assertThat(childBalancers).isEmpty(); + + // Simple case with one priority and one locality + EquivalentAddressGroup endpoint = makeAddress("endpoint-addr-1"); + LocalityLbEndpoints localityLbEndpoints = + LocalityLbEndpoints.create( + Arrays.asList( + LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true)), + 100 /* localityWeight */, 1 /* priority */); + xdsClient.deliverClusterLoadAssignment( + EDS_SERVICE_NAME1, + ImmutableMap.of(locality1, localityLbEndpoints)); + assertThat(childBalancers).hasSize(1); + FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); + assertThat(childBalancer.addresses).hasSize(1); + EquivalentAddressGroup addr = childBalancer.addresses.get(0); + assertThat(addr.getAddresses()).isEqualTo(endpoint.getAddresses()); + assertThat(childBalancer.name).isEqualTo(PRIORITY_POLICY_NAME); + PriorityLbConfig priorityLbConfig = (PriorityLbConfig) childBalancer.config; + assertThat(priorityLbConfig.priorities).containsExactly(CLUSTER1 + "[child1]"); + PriorityChildConfig priorityChildConfig = + Iterables.getOnlyElement(priorityLbConfig.childConfigs.values()); + + // The child config for priority should be outlier detection. + assertThat(priorityChildConfig.policySelection.getProvider().getPolicyName()) + .isEqualTo("outlier_detection_experimental"); + OutlierDetectionLoadBalancerConfig outlierDetectionConfig = + (OutlierDetectionLoadBalancerConfig) priorityChildConfig.policySelection.getConfig(); + + // The outlier detection config should faithfully represent what came down from xDS. + assertThat(outlierDetectionConfig.intervalNanos).isEqualTo(outlierDetection.intervalNanos()); + assertThat(outlierDetectionConfig.baseEjectionTimeNanos).isEqualTo( + outlierDetection.baseEjectionTimeNanos()); + assertThat(outlierDetectionConfig.baseEjectionTimeNanos).isEqualTo( + outlierDetection.baseEjectionTimeNanos()); + assertThat(outlierDetectionConfig.maxEjectionTimeNanos).isEqualTo( + outlierDetection.maxEjectionTimeNanos()); + assertThat(outlierDetectionConfig.maxEjectionPercent).isEqualTo( + outlierDetection.maxEjectionPercent()); + + OutlierDetectionLoadBalancerConfig.SuccessRateEjection successRateEjection + = outlierDetectionConfig.successRateEjection; + assertThat(successRateEjection.stdevFactor).isEqualTo( + outlierDetection.successRateEjection().stdevFactor()); + assertThat(successRateEjection.enforcementPercentage).isEqualTo( + outlierDetection.successRateEjection().enforcementPercentage()); + assertThat(successRateEjection.minimumHosts).isEqualTo( + outlierDetection.successRateEjection().minimumHosts()); + assertThat(successRateEjection.requestVolume).isEqualTo( + outlierDetection.successRateEjection().requestVolume()); + + OutlierDetectionLoadBalancerConfig.FailurePercentageEjection failurePercentageEjection + = outlierDetectionConfig.failurePercentageEjection; + assertThat(failurePercentageEjection.threshold).isEqualTo( + outlierDetection.failurePercentageEjection().threshold()); + assertThat(failurePercentageEjection.enforcementPercentage).isEqualTo( + outlierDetection.failurePercentageEjection().enforcementPercentage()); + assertThat(failurePercentageEjection.minimumHosts).isEqualTo( + outlierDetection.failurePercentageEjection().minimumHosts()); + assertThat(failurePercentageEjection.requestVolume).isEqualTo( + outlierDetection.failurePercentageEjection().requestVolume()); + + // The wrapped configuration should not have been tampered with. + ClusterImplConfig clusterImplConfig = + (ClusterImplConfig) outlierDetectionConfig.childPolicy.getConfig(); + assertClusterImplConfig(clusterImplConfig, CLUSTER1, EDS_SERVICE_NAME1, LRS_SERVER_INFO, 100L, + tlsContext, Collections.emptyList(), WRR_LOCALITY_POLICY_NAME); + WrrLocalityConfig wrrLocalityConfig = + (WrrLocalityConfig) clusterImplConfig.childPolicy.getConfig(); + assertThat(wrrLocalityConfig.childPolicy.getProvider().getPolicyName()).isEqualTo( + "least_request_experimental"); + + assertThat( + childBalancer.attributes.get(InternalXdsAttributes.ATTR_LOCALITY_WEIGHTS)).containsEntry( + locality1, 100); + } + + @Test public void onlyEdsClusters_receivedEndpoints() { ClusterResolverConfig config = new ClusterResolverConfig(