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(