xds: Use XdsDependencyManager for XdsNameResolver

Contributes to the gRFC A74 effort.
https://github.com/grpc/proposal/blob/master/A74-xds-config-tears.md

The alternative to using Mockito's ArgumentMatcher is to use Hamcrest.
However, Hamcrest did not impress me. ArgumentMatcher is trivial if you
don't care about the error message.

This fixes a pre-existing issue where ConfigSelector.releaseCluster
could revert the LB config back to using cluster manager after releasing
all RPCs using a cluster have committed.

Co-authored-by: Larry Safran <lsafran@google.com>
This commit is contained in:
Eric Anderson 2025-03-18 14:05:01 -07:00 committed by GitHub
parent e388ef3975
commit e80c197455
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 886 additions and 657 deletions

View File

@ -0,0 +1,118 @@
/*
* Copyright 2025 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import org.mockito.ArgumentMatcher;
/**
* Mockito matcher for {@link Status}.
*/
public final class StatusMatcher implements ArgumentMatcher<Status> {
public static StatusMatcher statusHasCode(ArgumentMatcher<Status.Code> codeMatcher) {
return new StatusMatcher(codeMatcher, null);
}
public static StatusMatcher statusHasCode(Status.Code code) {
return statusHasCode(new EqualsMatcher<>(code));
}
private final ArgumentMatcher<Status.Code> codeMatcher;
private final ArgumentMatcher<String> descriptionMatcher;
private StatusMatcher(
ArgumentMatcher<Status.Code> codeMatcher,
ArgumentMatcher<String> descriptionMatcher) {
this.codeMatcher = checkNotNull(codeMatcher, "codeMatcher");
this.descriptionMatcher = descriptionMatcher;
}
public StatusMatcher andDescription(ArgumentMatcher<String> descriptionMatcher) {
checkState(this.descriptionMatcher == null, "Already has a description matcher");
return new StatusMatcher(codeMatcher, descriptionMatcher);
}
public StatusMatcher andDescription(String description) {
return andDescription(new EqualsMatcher<>(description));
}
public StatusMatcher andDescriptionContains(String substring) {
return andDescription(new StringContainsMatcher(substring));
}
@Override
public boolean matches(Status status) {
return status != null
&& codeMatcher.matches(status.getCode())
&& (descriptionMatcher == null || descriptionMatcher.matches(status.getDescription()));
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("{code=");
sb.append(codeMatcher);
if (descriptionMatcher != null) {
sb.append(", description=");
sb.append(descriptionMatcher);
}
sb.append("}");
return sb.toString();
}
// Use instead of lambda for better error message.
static final class EqualsMatcher<T> implements ArgumentMatcher<T> {
private final T obj;
EqualsMatcher(T obj) {
this.obj = checkNotNull(obj, "obj");
}
@Override
public boolean matches(Object other) {
return obj.equals(other);
}
@Override
public String toString() {
return obj.toString();
}
}
static final class StringContainsMatcher implements ArgumentMatcher<String> {
private final String needle;
StringContainsMatcher(String needle) {
this.needle = checkNotNull(needle, "needle");
}
@Override
public boolean matches(String haystack) {
if (haystack == null) {
return false;
}
return haystack.contains(needle);
}
@Override
public String toString() {
return "contains " + needle;
}
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright 2025 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc;
import static com.google.common.base.Preconditions.checkNotNull;
import org.mockito.ArgumentMatcher;
/**
* Mockito matcher for {@link StatusOr}.
*/
public final class StatusOrMatcher<T> implements ArgumentMatcher<StatusOr<T>> {
public static <T> StatusOrMatcher<T> hasValue(ArgumentMatcher<T> valueMatcher) {
return new StatusOrMatcher<T>(checkNotNull(valueMatcher, "valueMatcher"), null);
}
public static <T> StatusOrMatcher<T> hasStatus(ArgumentMatcher<Status> statusMatcher) {
return new StatusOrMatcher<T>(null, checkNotNull(statusMatcher, "statusMatcher"));
}
private final ArgumentMatcher<T> valueMatcher;
private final ArgumentMatcher<Status> statusMatcher;
private StatusOrMatcher(ArgumentMatcher<T> valueMatcher, ArgumentMatcher<Status> statusMatcher) {
this.valueMatcher = valueMatcher;
this.statusMatcher = statusMatcher;
}
@Override
public boolean matches(StatusOr<T> statusOr) {
if (statusOr == null) {
return false;
}
if (statusOr.hasValue() != (valueMatcher != null)) {
return false;
}
if (valueMatcher != null) {
return valueMatcher.matches(statusOr.getValue());
} else {
return statusMatcher.matches(statusOr.getStatus());
}
}
@Override
public String toString() {
if (valueMatcher != null) {
return "{value=" + valueMatcher + "}";
} else {
return "{status=" + statusMatcher + "}";
}
}
}

View File

@ -36,6 +36,22 @@ final class XdsAttributes {
static final Attributes.Key<ObjectPool<XdsClient>> XDS_CLIENT_POOL = static final Attributes.Key<ObjectPool<XdsClient>> XDS_CLIENT_POOL =
Attributes.Key.create("io.grpc.xds.XdsAttributes.xdsClientPool"); Attributes.Key.create("io.grpc.xds.XdsAttributes.xdsClientPool");
/**
* Attribute key for passing around the latest XdsConfig across NameResolver/LoadBalancers.
*/
@NameResolver.ResolutionResultAttr
static final Attributes.Key<XdsConfig> XDS_CONFIG =
Attributes.Key.create("io.grpc.xds.XdsAttributes.xdsConfig");
/**
* Attribute key for passing around the XdsDependencyManager across NameResolver/LoadBalancers.
*/
@NameResolver.ResolutionResultAttr
static final Attributes.Key<XdsConfig.XdsClusterSubscriptionRegistry>
XDS_CLUSTER_SUBSCRIPT_REGISTRY =
Attributes.Key.create("io.grpc.xds.XdsAttributes.xdsConfig.XdsClusterSubscriptionRegistry");
/** /**
* Attribute key for obtaining the global provider that provides atomics for aggregating * Attribute key for obtaining the global provider that provides atomics for aggregating
* outstanding RPCs sent to each cluster. * outstanding RPCs sent to each cluster.

View File

@ -26,9 +26,9 @@ import io.grpc.xds.XdsListenerResource.LdsUpdate;
import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate; import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate;
import java.io.Closeable; import java.io.Closeable;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
/** /**
* Represents the xDS configuration tree for a specified Listener. * Represents the xDS configuration tree for a specified Listener.
@ -178,13 +178,22 @@ final class XdsConfig {
public StatusOr<EdsUpdate> getEndpoint() { public StatusOr<EdsUpdate> getEndpoint() {
return endpoint; return endpoint;
} }
@Override
public String toString() {
if (endpoint.hasValue()) {
return "EndpointConfig{endpoint=" + endpoint.getValue() + "}";
} else {
return "EndpointConfig{error=" + endpoint.getStatus() + "}";
}
}
} }
// The list of leaf clusters for an aggregate cluster. // The list of leaf clusters for an aggregate cluster.
static final class AggregateConfig implements ClusterChild { static final class AggregateConfig implements ClusterChild {
private final List<String> leafNames; private final Set<String> leafNames;
public AggregateConfig(List<String> leafNames) { public AggregateConfig(Set<String> leafNames) {
this.leafNames = checkNotNull(leafNames, "leafNames"); this.leafNames = checkNotNull(leafNames, "leafNames");
} }
@ -234,6 +243,7 @@ final class XdsConfig {
XdsConfig build() { XdsConfig build() {
checkNotNull(listener, "listener"); checkNotNull(listener, "listener");
checkNotNull(route, "route"); checkNotNull(route, "route");
checkNotNull(virtualHost, "virtualHost");
return new XdsConfig(listener, route, clusters, virtualHost); return new XdsConfig(listener, route, clusters, virtualHost);
} }
} }

View File

@ -22,9 +22,9 @@ import static io.grpc.xds.client.XdsClient.ResourceUpdate;
import static io.grpc.xds.client.XdsLogger.XdsLogLevel.DEBUG; import static io.grpc.xds.client.XdsLogger.XdsLogLevel.DEBUG;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import io.grpc.InternalLogId; import io.grpc.InternalLogId;
import io.grpc.NameResolver;
import io.grpc.Status; import io.grpc.Status;
import io.grpc.StatusOr; import io.grpc.StatusOr;
import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext;
@ -39,7 +39,6 @@ import io.grpc.xds.client.XdsLogger;
import io.grpc.xds.client.XdsResourceType; import io.grpc.xds.client.XdsResourceType;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@ -47,6 +46,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -67,25 +67,28 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
private final InternalLogId logId; private final InternalLogId logId;
private final XdsLogger logger; private final XdsLogger logger;
private XdsConfig lastXdsConfig = null; private StatusOr<XdsConfig> lastUpdate = null;
private final Map<XdsResourceType<?>, TypeWatchers<?>> resourceWatchers = new HashMap<>(); private final Map<XdsResourceType<?>, TypeWatchers<?>> resourceWatchers = new HashMap<>();
XdsDependencyManager(XdsClient xdsClient, XdsConfigWatcher xdsConfigWatcher, XdsDependencyManager(XdsClient xdsClient, XdsConfigWatcher xdsConfigWatcher,
SynchronizationContext syncContext, String dataPlaneAuthority, SynchronizationContext syncContext, String dataPlaneAuthority,
String listenerName) { String listenerName, NameResolver.Args nameResolverArgs,
ScheduledExecutorService scheduler) {
logId = InternalLogId.allocate("xds-dependency-manager", listenerName); logId = InternalLogId.allocate("xds-dependency-manager", listenerName);
logger = XdsLogger.withLogId(logId); logger = XdsLogger.withLogId(logId);
this.xdsClient = checkNotNull(xdsClient, "xdsClient"); this.xdsClient = checkNotNull(xdsClient, "xdsClient");
this.xdsConfigWatcher = checkNotNull(xdsConfigWatcher, "xdsConfigWatcher"); this.xdsConfigWatcher = checkNotNull(xdsConfigWatcher, "xdsConfigWatcher");
this.syncContext = checkNotNull(syncContext, "syncContext"); this.syncContext = checkNotNull(syncContext, "syncContext");
this.dataPlaneAuthority = checkNotNull(dataPlaneAuthority, "dataPlaneAuthority"); this.dataPlaneAuthority = checkNotNull(dataPlaneAuthority, "dataPlaneAuthority");
checkNotNull(nameResolverArgs, "nameResolverArgs");
checkNotNull(scheduler, "scheduler");
// start the ball rolling // start the ball rolling
syncContext.execute(() -> addWatcher(new LdsWatcher(listenerName))); syncContext.execute(() -> addWatcher(new LdsWatcher(listenerName)));
} }
public static String toContextStr(String typeName, String resourceName) { public static String toContextStr(String typeName, String resourceName) {
return typeName + " resource: " + resourceName; return typeName + " resource " + resourceName;
} }
@Override @Override
@ -225,7 +228,7 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
XdsClusterResource.CdsUpdate cdsUpdate = root.getData().getValue(); XdsClusterResource.CdsUpdate cdsUpdate = root.getData().getValue();
switch (cdsUpdate.clusterType()) { switch (cdsUpdate.clusterType()) {
case EDS: case EDS:
String edsServiceName = cdsUpdate.edsServiceName(); String edsServiceName = root.getEdsServiceName();
EdsWatcher edsWatcher = EdsWatcher edsWatcher =
(EdsWatcher) resourceWatchers.get(ENDPOINT_RESOURCE).watchers.get(edsServiceName); (EdsWatcher) resourceWatchers.get(ENDPOINT_RESOURCE).watchers.get(edsServiceName);
cancelEdsWatcher(edsWatcher, root); cancelEdsWatcher(edsWatcher, root);
@ -240,7 +243,7 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
} }
break; break;
case LOGICAL_DNS: case LOGICAL_DNS:
// no eds needed // no eds needed, so everything happens in cancelCdsWatcher()
break; break;
default: default:
throw new AssertionError("Unknown cluster type: " + cdsUpdate.clusterType()); throw new AssertionError("Unknown cluster type: " + cdsUpdate.clusterType());
@ -260,54 +263,61 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
return; return;
} }
XdsConfig newConfig = buildConfig(); StatusOr<XdsConfig> newUpdate = buildUpdate();
if (Objects.equals(newConfig, lastXdsConfig)) { if (Objects.equals(newUpdate, lastUpdate)) {
return; return;
} }
lastXdsConfig = newConfig; assert newUpdate.hasValue()
xdsConfigWatcher.onUpdate(lastXdsConfig); || (newUpdate.getStatus().getCode() == Status.Code.UNAVAILABLE
|| newUpdate.getStatus().getCode() == Status.Code.INTERNAL);
lastUpdate = newUpdate;
xdsConfigWatcher.onUpdate(lastUpdate);
} }
@VisibleForTesting @VisibleForTesting
XdsConfig buildConfig() { StatusOr<XdsConfig> buildUpdate() {
XdsConfig.XdsConfigBuilder builder = new XdsConfig.XdsConfigBuilder(); XdsConfig.XdsConfigBuilder builder = new XdsConfig.XdsConfigBuilder();
// Iterate watchers and build the XdsConfig // Iterate watchers and build the XdsConfig
// Will only be 1 listener and 1 route resource // Will only be 1 listener and 1 route resource
VirtualHost activeVirtualHost = getActiveVirtualHost(); RdsUpdateSupplier routeSource = null;
for (XdsWatcherBase<?> xdsWatcherBase : for (XdsWatcherBase<XdsListenerResource.LdsUpdate> ldsWatcher :
resourceWatchers.get(XdsListenerResource.getInstance()).watchers.values()) { getWatchers(XdsListenerResource.getInstance()).values()) {
XdsListenerResource.LdsUpdate ldsUpdate = ((LdsWatcher) xdsWatcherBase).getData().getValue(); if (!ldsWatcher.getData().hasValue()) {
return StatusOr.fromStatus(ldsWatcher.getData().getStatus());
}
XdsListenerResource.LdsUpdate ldsUpdate = ldsWatcher.getData().getValue();
builder.setListener(ldsUpdate); builder.setListener(ldsUpdate);
if (activeVirtualHost == null) { routeSource = ((LdsWatcher) ldsWatcher).getRouteSource();
activeVirtualHost = RoutingUtils.findVirtualHostForHostName(
ldsUpdate.httpConnectionManager().virtualHosts(), dataPlaneAuthority);
}
if (ldsUpdate.httpConnectionManager() != null
&& ldsUpdate.httpConnectionManager().virtualHosts() != null) {
RdsUpdate rdsUpdate = new RdsUpdate(ldsUpdate.httpConnectionManager().virtualHosts());
builder.setRoute(rdsUpdate);
}
} }
resourceWatchers.get(XdsRouteConfigureResource.getInstance()).watchers.values().stream() StatusOr<RdsUpdate> statusOrRdsUpdate = routeSource.getRdsUpdate();
.map(watcher -> (RdsWatcher) watcher) if (!statusOrRdsUpdate.hasValue()) {
.forEach(watcher -> builder.setRoute(watcher.getData().getValue())); return StatusOr.fromStatus(statusOrRdsUpdate.getStatus());
}
RdsUpdate rdsUpdate = statusOrRdsUpdate.getValue();
builder.setRoute(rdsUpdate);
VirtualHost activeVirtualHost =
RoutingUtils.findVirtualHostForHostName(rdsUpdate.virtualHosts, dataPlaneAuthority);
if (activeVirtualHost == null) {
String error = "Failed to find virtual host matching hostname: " + dataPlaneAuthority;
return StatusOr.fromStatus(Status.UNAVAILABLE.withDescription(error));
}
builder.setVirtualHost(activeVirtualHost); builder.setVirtualHost(activeVirtualHost);
Map<String, ? extends XdsWatcherBase<?>> edsWatchers = Map<String, XdsWatcherBase<XdsEndpointResource.EdsUpdate>> edsWatchers =
resourceWatchers.get(ENDPOINT_RESOURCE).watchers; getWatchers(ENDPOINT_RESOURCE);
Map<String, ? extends XdsWatcherBase<?>> cdsWatchers = Map<String, XdsWatcherBase<XdsClusterResource.CdsUpdate>> cdsWatchers =
resourceWatchers.get(CLUSTER_RESOURCE).watchers; getWatchers(CLUSTER_RESOURCE);
// Only care about aggregates from LDS/RDS or subscriptions and the leaf clusters // Only care about aggregates from LDS/RDS or subscriptions and the leaf clusters
List<String> topLevelClusters = List<String> topLevelClusters =
cdsWatchers.values().stream() cdsWatchers.values().stream()
.filter(XdsDependencyManager::isTopLevelCluster) .filter(XdsDependencyManager::isTopLevelCluster)
.map(w -> w.resourceName()) .map(XdsWatcherBase<?>::resourceName)
.distinct()
.collect(Collectors.toList()); .collect(Collectors.toList());
// Flatten multi-level aggregates into lists of leaf clusters // Flatten multi-level aggregates into lists of leaf clusters
@ -316,43 +326,60 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
addLeavesToBuilder(builder, edsWatchers, leafNames); addLeavesToBuilder(builder, edsWatchers, leafNames);
return builder.build(); return StatusOr.fromValue(builder.build());
} }
private void addLeavesToBuilder(XdsConfig.XdsConfigBuilder builder, private <T extends ResourceUpdate> Map<String, XdsWatcherBase<T>> getWatchers(
Map<String, ? extends XdsWatcherBase<?>> edsWatchers, XdsResourceType<T> resourceType) {
Set<String> leafNames) { TypeWatchers<?> typeWatchers = resourceWatchers.get(resourceType);
if (typeWatchers == null) {
return Collections.emptyMap();
}
assert typeWatchers.resourceType == resourceType;
@SuppressWarnings("unchecked")
TypeWatchers<T> tTypeWatchers = (TypeWatchers<T>) typeWatchers;
return tTypeWatchers.watchers;
}
private void addLeavesToBuilder(
XdsConfig.XdsConfigBuilder builder,
Map<String, XdsWatcherBase<XdsEndpointResource.EdsUpdate>> edsWatchers,
Set<String> leafNames) {
for (String clusterName : leafNames) { for (String clusterName : leafNames) {
CdsWatcher cdsWatcher = getCluster(clusterName); CdsWatcher cdsWatcher = getCluster(clusterName);
StatusOr<XdsClusterResource.CdsUpdate> cdsUpdateOr = cdsWatcher.getData(); StatusOr<XdsClusterResource.CdsUpdate> cdsUpdateOr = cdsWatcher.getData();
if (cdsUpdateOr.hasValue()) { if (!cdsUpdateOr.hasValue()) {
XdsClusterResource.CdsUpdate cdsUpdate = cdsUpdateOr.getValue();
if (cdsUpdate.clusterType() == ClusterType.EDS) {
EdsWatcher edsWatcher = (EdsWatcher) edsWatchers.get(cdsUpdate.edsServiceName());
if (edsWatcher != null) {
EndpointConfig child = new EndpointConfig(edsWatcher.getData());
builder.addCluster(clusterName, StatusOr.fromValue(
new XdsConfig.XdsClusterConfig(clusterName, cdsUpdate, child)));
} else {
builder.addCluster(clusterName, StatusOr.fromStatus(Status.UNAVAILABLE.withDescription(
"EDS resource not found for cluster " + clusterName)));
}
} else if (cdsUpdate.clusterType() == ClusterType.LOGICAL_DNS) {
// TODO get the resolved endpoint configuration
builder.addCluster(clusterName, StatusOr.fromValue(
new XdsConfig.XdsClusterConfig(clusterName, cdsUpdate, new EndpointConfig(null))));
}
} else {
builder.addCluster(clusterName, StatusOr.fromStatus(cdsUpdateOr.getStatus())); builder.addCluster(clusterName, StatusOr.fromStatus(cdsUpdateOr.getStatus()));
continue;
}
XdsClusterResource.CdsUpdate cdsUpdate = cdsUpdateOr.getValue();
if (cdsUpdate.clusterType() == ClusterType.EDS) {
XdsWatcherBase<XdsEndpointResource.EdsUpdate> edsWatcher =
edsWatchers.get(cdsWatcher.getEdsServiceName());
EndpointConfig child;
if (edsWatcher != null) {
child = new EndpointConfig(edsWatcher.getData());
} else {
child = new EndpointConfig(StatusOr.fromStatus(Status.INTERNAL.withDescription(
"EDS resource not found for cluster " + clusterName)));
}
builder.addCluster(clusterName, StatusOr.fromValue(
new XdsConfig.XdsClusterConfig(clusterName, cdsUpdate, child)));
} else if (cdsUpdate.clusterType() == ClusterType.LOGICAL_DNS) {
builder.addCluster(clusterName, StatusOr.fromStatus(
Status.INTERNAL.withDescription("Logical DNS in dependency manager unsupported")));
} }
} }
} }
// Adds the top-level clusters to the builder and returns the leaf cluster names // Adds the top-level clusters to the builder and returns the leaf cluster names
private Set<String> addTopLevelClustersToBuilder( private Set<String> addTopLevelClustersToBuilder(
XdsConfig.XdsConfigBuilder builder, Map<String, ? extends XdsWatcherBase<?>> edsWatchers, XdsConfig.XdsConfigBuilder builder,
Map<String, ? extends XdsWatcherBase<?>> cdsWatchers, List<String> topLevelClusters) { Map<String, XdsWatcherBase<XdsEndpointResource.EdsUpdate>> edsWatchers,
Map<String, XdsWatcherBase<XdsClusterResource.CdsUpdate>> cdsWatchers,
List<String> topLevelClusters) {
Set<String> leafClusterNames = new HashSet<>(); Set<String> leafClusterNames = new HashSet<>();
for (String clusterName : topLevelClusters) { for (String clusterName : topLevelClusters) {
@ -367,23 +394,25 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
XdsConfig.XdsClusterConfig.ClusterChild child; XdsConfig.XdsClusterConfig.ClusterChild child;
switch (cdsUpdate.clusterType()) { switch (cdsUpdate.clusterType()) {
case AGGREGATE: case AGGREGATE:
List<String> leafNames = getLeafNames(cdsUpdate); Set<String> leafNames = new HashSet<>();
addLeafNames(leafNames, cdsUpdate);
child = new AggregateConfig(leafNames); child = new AggregateConfig(leafNames);
leafClusterNames.addAll(leafNames); leafClusterNames.addAll(leafNames);
break; break;
case EDS: case EDS:
EdsWatcher edsWatcher = (EdsWatcher) edsWatchers.get(cdsUpdate.edsServiceName()); XdsWatcherBase<XdsEndpointResource.EdsUpdate> edsWatcher =
edsWatchers.get(cdsWatcher.getEdsServiceName());
if (edsWatcher != null) { if (edsWatcher != null) {
child = new EndpointConfig(edsWatcher.getData()); child = new EndpointConfig(edsWatcher.getData());
} else { } else {
builder.addCluster(clusterName, StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( child = new EndpointConfig(StatusOr.fromStatus(Status.INTERNAL.withDescription(
"EDS resource not found for cluster " + clusterName))); "EDS resource not found for cluster " + clusterName)));
continue;
} }
break; break;
case LOGICAL_DNS: case LOGICAL_DNS:
// TODO get the resolved endpoint configuration // TODO get the resolved endpoint configuration
child = new EndpointConfig(null); child = new EndpointConfig(StatusOr.fromStatus(
Status.INTERNAL.withDescription("Logical DNS in dependency manager unsupported")));
break; break;
default: default:
throw new IllegalStateException("Unexpected value: " + cdsUpdate.clusterType()); throw new IllegalStateException("Unexpected value: " + cdsUpdate.clusterType());
@ -395,29 +424,26 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
return leafClusterNames; return leafClusterNames;
} }
private List<String> getLeafNames(XdsClusterResource.CdsUpdate cdsUpdate) { private void addLeafNames(Set<String> leafNames, XdsClusterResource.CdsUpdate cdsUpdate) {
List<String> childNames = new ArrayList<>();
for (String cluster : cdsUpdate.prioritizedClusterNames()) { for (String cluster : cdsUpdate.prioritizedClusterNames()) {
if (leafNames.contains(cluster)) {
continue;
}
StatusOr<XdsClusterResource.CdsUpdate> data = getCluster(cluster).getData(); StatusOr<XdsClusterResource.CdsUpdate> data = getCluster(cluster).getData();
if (data == null || !data.hasValue() || data.getValue() == null) { if (data == null || !data.hasValue() || data.getValue() == null) {
childNames.add(cluster); leafNames.add(cluster);
continue; continue;
} }
if (data.getValue().clusterType() == ClusterType.AGGREGATE) { if (data.getValue().clusterType() == ClusterType.AGGREGATE) {
childNames.addAll(getLeafNames(data.getValue())); addLeafNames(leafNames, data.getValue());
} else { } else {
childNames.add(cluster); leafNames.add(cluster);
} }
} }
return childNames;
} }
private static boolean isTopLevelCluster(XdsWatcherBase<?> cdsWatcher) { private static boolean isTopLevelCluster(
if (! (cdsWatcher instanceof CdsWatcher)) { XdsWatcherBase<XdsClusterResource.CdsUpdate> cdsWatcher) {
return false;
}
return ((CdsWatcher)cdsWatcher).parentContexts.values().stream() return ((CdsWatcher)cdsWatcher).parentContexts.values().stream()
.anyMatch(depth -> depth == 1); .anyMatch(depth -> depth == 1);
} }
@ -454,17 +480,11 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
} }
private void updateRoutes(List<VirtualHost> virtualHosts, Object newParentContext, private void updateRoutes(List<VirtualHost> virtualHosts, Object newParentContext,
VirtualHost oldVirtualHost, boolean sameParentContext) { List<VirtualHost> oldVirtualHosts, boolean sameParentContext) {
VirtualHost oldVirtualHost =
RoutingUtils.findVirtualHostForHostName(oldVirtualHosts, dataPlaneAuthority);
VirtualHost virtualHost = VirtualHost virtualHost =
RoutingUtils.findVirtualHostForHostName(virtualHosts, dataPlaneAuthority); RoutingUtils.findVirtualHostForHostName(virtualHosts, dataPlaneAuthority);
if (virtualHost == null) {
String error = "Failed to find virtual host matching hostname: " + dataPlaneAuthority;
logger.log(XdsLogger.XdsLogLevel.WARNING, error);
cleanUpRoutes();
xdsConfigWatcher.onError(
"xDS node ID:" + dataPlaneAuthority, Status.UNAVAILABLE.withDescription(error));
return;
}
Set<String> newClusters = getClusterNamesFromVirtualHost(virtualHost); Set<String> newClusters = getClusterNamesFromVirtualHost(virtualHost);
Set<String> oldClusters = getClusterNamesFromVirtualHost(oldVirtualHost); Set<String> oldClusters = getClusterNamesFromVirtualHost(oldVirtualHost);
@ -482,6 +502,10 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
} }
} }
private String nodeInfo() {
return " nodeID: " + xdsClient.getBootstrapInfo().node().getId();
}
private static Set<String> getClusterNamesFromVirtualHost(VirtualHost virtualHost) { private static Set<String> getClusterNamesFromVirtualHost(VirtualHost virtualHost) {
if (virtualHost == null) { if (virtualHost == null) {
return Collections.emptySet(); return Collections.emptySet();
@ -506,46 +530,6 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
return clusters; return clusters;
} }
@Nullable
private VirtualHost getActiveVirtualHost() {
TypeWatchers<?> rdsWatchers = resourceWatchers.get(XdsRouteConfigureResource.getInstance());
if (rdsWatchers == null) {
return null;
}
RdsWatcher activeRdsWatcher =
(RdsWatcher) rdsWatchers.watchers.values().stream().findFirst().orElse(null);
if (activeRdsWatcher == null || activeRdsWatcher.missingResult()
|| !activeRdsWatcher.getData().hasValue()) {
return null;
}
return RoutingUtils.findVirtualHostForHostName(
activeRdsWatcher.getData().getValue().virtualHosts, dataPlaneAuthority);
}
// Must be in SyncContext
private void cleanUpRoutes() {
// Remove RdsWatcher & CDS Watchers
TypeWatchers<?> rdsResourceWatcher =
resourceWatchers.get(XdsRouteConfigureResource.getInstance());
if (rdsResourceWatcher == null || rdsResourceWatcher.watchers.isEmpty()) {
return;
}
XdsWatcherBase<?> watcher = rdsResourceWatcher.watchers.values().stream().findFirst().get();
cancelWatcher(watcher);
// Remove CdsWatchers pointed to by the RdsWatcher
RdsWatcher rdsWatcher = (RdsWatcher) watcher;
for (String cName : rdsWatcher.getCdsNames()) {
CdsWatcher cdsWatcher = getCluster(cName);
if (cdsWatcher != null) {
cancelClusterWatcherTree(cdsWatcher, rdsWatcher);
}
}
}
private CdsWatcher getCluster(String clusterName) { private CdsWatcher getCluster(String clusterName) {
return (CdsWatcher) resourceWatchers.get(CLUSTER_RESOURCE).watchers.get(clusterName); return (CdsWatcher) resourceWatchers.get(CLUSTER_RESOURCE).watchers.get(clusterName);
} }
@ -565,16 +549,11 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
} }
public interface XdsConfigWatcher { public interface XdsConfigWatcher {
/**
void onUpdate(XdsConfig config); * An updated XdsConfig or RPC-safe Status. The status code will be either UNAVAILABLE or
* INTERNAL.
// These 2 methods are invoked when there is an error or */
// does-not-exist on LDS or RDS only. The context will be a void onUpdate(StatusOr<XdsConfig> config);
// human-readable string indicating the scope in which the error
// occurred (e.g., the resource type and name).
void onError(String resourceContext, Status status);
void onResourceDoesNotExist(String resourceContext);
} }
private class ClusterSubscription implements Closeable { private class ClusterSubscription implements Closeable {
@ -594,7 +573,7 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
} }
} }
private abstract static class XdsWatcherBase<T extends ResourceUpdate> private abstract class XdsWatcherBase<T extends ResourceUpdate>
implements ResourceWatcher<T> { implements ResourceWatcher<T> {
private final XdsResourceType<T> type; private final XdsResourceType<T> type;
private final String resourceName; private final String resourceName;
@ -612,12 +591,25 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
@Override @Override
public void onError(Status error) { public void onError(Status error) {
checkNotNull(error, "error"); checkNotNull(error, "error");
setDataAsStatus(error); // Don't update configuration on error, if we've already received configuration
if (!hasDataValue()) {
setDataAsStatus(Status.UNAVAILABLE.withDescription(
String.format("Error retrieving %s: %s: %s",
toContextString(), error.getCode(), error.getDescription())));
maybePublishConfig();
}
} }
protected void handleDoesNotExist(String resourceName) { @Override
public void onResourceDoesNotExist(String resourceName) {
if (cancelled) {
return;
}
checkArgument(this.resourceName.equals(resourceName), "Resource name does not match"); checkArgument(this.resourceName.equals(resourceName), "Resource name does not match");
setDataAsStatus(Status.UNAVAILABLE.withDescription("No " + toContextString())); setDataAsStatus(Status.UNAVAILABLE.withDescription(
toContextString() + " does not exist" + nodeInfo()));
maybePublishConfig();
} }
boolean missingResult() { boolean missingResult() {
@ -647,12 +639,17 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
this.data = StatusOr.fromStatus(status); this.data = StatusOr.fromStatus(status);
} }
String toContextString() { public String toContextString() {
return toContextStr(type.typeName(), resourceName); return toContextStr(type.typeName(), resourceName);
} }
} }
private class LdsWatcher extends XdsWatcherBase<XdsListenerResource.LdsUpdate> { private interface RdsUpdateSupplier {
StatusOr<RdsUpdate> getRdsUpdate();
}
private class LdsWatcher extends XdsWatcherBase<XdsListenerResource.LdsUpdate>
implements RdsUpdateSupplier {
String rdsName; String rdsName;
private LdsWatcher(String resourceName) { private LdsWatcher(String resourceName) {
@ -664,9 +661,20 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
checkNotNull(update, "update"); checkNotNull(update, "update");
HttpConnectionManager httpConnectionManager = update.httpConnectionManager(); HttpConnectionManager httpConnectionManager = update.httpConnectionManager();
List<VirtualHost> virtualHosts = httpConnectionManager.virtualHosts(); List<VirtualHost> virtualHosts;
String rdsName = httpConnectionManager.rdsName(); String rdsName;
VirtualHost activeVirtualHost = getActiveVirtualHost(); if (httpConnectionManager == null) {
// TCP listener. Unsupported config
virtualHosts = Collections.emptyList(); // Not null, to not delegate to RDS
rdsName = null;
} else {
virtualHosts = httpConnectionManager.virtualHosts();
rdsName = httpConnectionManager.rdsName();
}
StatusOr<RdsUpdate> activeRdsUpdate = getRouteSource().getRdsUpdate();
List<VirtualHost> activeVirtualHosts = activeRdsUpdate.hasValue()
? activeRdsUpdate.getValue().virtualHosts
: Collections.emptyList();
boolean changedRdsName = !Objects.equals(rdsName, this.rdsName); boolean changedRdsName = !Objects.equals(rdsName, this.rdsName);
if (changedRdsName) { if (changedRdsName) {
@ -675,10 +683,9 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
if (virtualHosts != null) { if (virtualHosts != null) {
// No RDS watcher since we are getting RDS updates via LDS // No RDS watcher since we are getting RDS updates via LDS
updateRoutes(virtualHosts, this, activeVirtualHost, this.rdsName == null); updateRoutes(virtualHosts, this, activeVirtualHosts, this.rdsName == null);
this.rdsName = null; this.rdsName = null;
} else if (changedRdsName) { } else if (changedRdsName) {
cleanUpRdsWatcher();
this.rdsName = rdsName; this.rdsName = rdsName;
addWatcher(new RdsWatcher(rdsName)); addWatcher(new RdsWatcher(rdsName));
logger.log(XdsLogger.XdsLogLevel.INFO, "Start watching RDS resource {0}", rdsName); logger.log(XdsLogger.XdsLogLevel.INFO, "Start watching RDS resource {0}", rdsName);
@ -688,20 +695,18 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
maybePublishConfig(); maybePublishConfig();
} }
@Override
public void onError(Status error) {
super.onError(checkNotNull(error, "error"));
xdsConfigWatcher.onError(toContextString(), error);
}
@Override @Override
public void onResourceDoesNotExist(String resourceName) { public void onResourceDoesNotExist(String resourceName) {
if (cancelled) { if (cancelled) {
return; return;
} }
handleDoesNotExist(resourceName); checkArgument(resourceName().equals(resourceName), "Resource name does not match");
xdsConfigWatcher.onResourceDoesNotExist(toContextString()); setDataAsStatus(Status.UNAVAILABLE.withDescription(
toContextString() + " does not exist" + nodeInfo()));
cleanUpRdsWatcher();
rdsName = null;
maybePublishConfig();
} }
private void cleanUpRdsWatcher() { private void cleanUpRdsWatcher() {
@ -711,8 +716,7 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
logger.log(XdsLogger.XdsLogLevel.DEBUG, "Stop watching RDS resource {0}", rdsName); logger.log(XdsLogger.XdsLogLevel.DEBUG, "Stop watching RDS resource {0}", rdsName);
// Cleanup clusters (as appropriate) that had the old rds watcher as a parent // Cleanup clusters (as appropriate) that had the old rds watcher as a parent
if (!oldRdsWatcher.hasDataValue() || !oldRdsWatcher.getData().hasValue() if (!oldRdsWatcher.hasDataValue() || resourceWatchers.get(CLUSTER_RESOURCE) == null) {
|| resourceWatchers.get(CLUSTER_RESOURCE) == null) {
return; return;
} }
for (XdsWatcherBase<?> watcher : for (XdsWatcherBase<?> watcher :
@ -723,16 +727,58 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
} }
private RdsWatcher getRdsWatcher() { private RdsWatcher getRdsWatcher() {
TypeWatchers<?> watchers = resourceWatchers.get(XdsRouteConfigureResource.getInstance()); if (rdsName == null) {
if (watchers == null || rdsName == null || watchers.watchers.isEmpty()) { return null;
}
TypeWatchers<?> watchers = resourceWatchers.get(XdsRouteConfigureResource.getInstance());
if (watchers == null) {
return null; return null;
} }
return (RdsWatcher) watchers.watchers.get(rdsName); return (RdsWatcher) watchers.watchers.get(rdsName);
} }
public RdsUpdateSupplier getRouteSource() {
if (!hasDataValue()) {
return this;
}
HttpConnectionManager hcm = getData().getValue().httpConnectionManager();
if (hcm == null) {
return this;
}
List<VirtualHost> virtualHosts = hcm.virtualHosts();
if (virtualHosts != null) {
return this;
}
RdsWatcher rdsWatcher = getRdsWatcher();
assert rdsWatcher != null;
return rdsWatcher;
}
@Override
public StatusOr<RdsUpdate> getRdsUpdate() {
if (missingResult()) {
return StatusOr.fromStatus(Status.UNAVAILABLE.withDescription("Not yet loaded"));
}
if (!getData().hasValue()) {
return StatusOr.fromStatus(getData().getStatus());
}
HttpConnectionManager hcm = getData().getValue().httpConnectionManager();
if (hcm == null) {
return StatusOr.fromStatus(
Status.UNAVAILABLE.withDescription("Not an API listener" + nodeInfo()));
}
List<VirtualHost> virtualHosts = hcm.virtualHosts();
if (virtualHosts == null) {
// Code shouldn't trigger this case, as it should be calling RdsWatcher instead. This would
// be easily implemented with getRdsWatcher().getRdsUpdate(), but getting here is likely a
// bug
return StatusOr.fromStatus(Status.INTERNAL.withDescription("Routes are in RDS, not LDS"));
}
return StatusOr.fromValue(new RdsUpdate(virtualHosts));
}
} }
private class RdsWatcher extends XdsWatcherBase<RdsUpdate> { private class RdsWatcher extends XdsWatcherBase<RdsUpdate> implements RdsUpdateSupplier {
public RdsWatcher(String resourceName) { public RdsWatcher(String resourceName) {
super(XdsRouteConfigureResource.getInstance(), checkNotNull(resourceName, "resourceName")); super(XdsRouteConfigureResource.getInstance(), checkNotNull(resourceName, "resourceName"));
@ -741,37 +787,20 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
@Override @Override
public void onChanged(RdsUpdate update) { public void onChanged(RdsUpdate update) {
checkNotNull(update, "update"); checkNotNull(update, "update");
RdsUpdate oldData = hasDataValue() ? getData().getValue() : null; List<VirtualHost> oldVirtualHosts = hasDataValue()
VirtualHost oldVirtualHost = ? getData().getValue().virtualHosts
(oldData != null) : Collections.emptyList();
? RoutingUtils.findVirtualHostForHostName(oldData.virtualHosts, dataPlaneAuthority)
: null;
setData(update); setData(update);
updateRoutes(update.virtualHosts, this, oldVirtualHost, true); updateRoutes(update.virtualHosts, this, oldVirtualHosts, true);
maybePublishConfig(); maybePublishConfig();
} }
@Override @Override
public void onError(Status error) { public StatusOr<RdsUpdate> getRdsUpdate() {
super.onError(checkNotNull(error, "error")); if (missingResult()) {
xdsConfigWatcher.onError(toContextString(), error); return StatusOr.fromStatus(Status.UNAVAILABLE.withDescription("Not yet loaded"));
}
@Override
public void onResourceDoesNotExist(String resourceName) {
if (cancelled) {
return;
} }
handleDoesNotExist(checkNotNull(resourceName, "resourceName")); return getData();
xdsConfigWatcher.onResourceDoesNotExist(toContextString());
}
ImmutableList<String> getCdsNames() {
if (!hasDataValue() || getData().getValue().virtualHosts == null) {
return ImmutableList.of();
}
return ImmutableList.copyOf(getClusterNamesFromVirtualHost(getActiveVirtualHost()));
} }
} }
@ -789,7 +818,7 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
switch (update.clusterType()) { switch (update.clusterType()) {
case EDS: case EDS:
setData(update); setData(update);
if (!addEdsWatcher(update.edsServiceName(), this)) { if (!addEdsWatcher(getEdsServiceName(), this)) {
maybePublishConfig(); maybePublishConfig();
} }
break; break;
@ -805,14 +834,15 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
logger.log(XdsLogger.XdsLogLevel.WARNING, logger.log(XdsLogger.XdsLogLevel.WARNING,
"Cluster recursion depth limit exceeded for cluster {0}", resourceName()); "Cluster recursion depth limit exceeded for cluster {0}", resourceName());
Status error = Status.UNAVAILABLE.withDescription( Status error = Status.UNAVAILABLE.withDescription(
"aggregate cluster graph exceeds max depth"); "aggregate cluster graph exceeds max depth at " + resourceName() + nodeInfo());
setDataAsStatus(error); setDataAsStatus(error);
} }
if (hasDataValue()) { if (hasDataValue()) {
Set<String> oldNames = new HashSet<>(getData().getValue().prioritizedClusterNames()); Set<String> oldNames = getData().getValue().clusterType() == ClusterType.AGGREGATE
? new HashSet<>(getData().getValue().prioritizedClusterNames())
: Collections.emptySet();
Set<String> newNames = new HashSet<>(update.prioritizedClusterNames()); Set<String> newNames = new HashSet<>(update.prioritizedClusterNames());
Set<String> deletedClusters = Sets.difference(oldNames, newNames); Set<String> deletedClusters = Sets.difference(oldNames, newNames);
deletedClusters.forEach((cluster) deletedClusters.forEach((cluster)
-> cancelClusterWatcherTree(getCluster(cluster), parentContext)); -> cancelClusterWatcherTree(getCluster(cluster), parentContext));
@ -838,19 +868,20 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
break; break;
default: default:
Status error = Status.UNAVAILABLE.withDescription( Status error = Status.UNAVAILABLE.withDescription(
"aggregate cluster graph exceeds max depth"); "aggregate cluster graph exceeds max depth at " + resourceName() + nodeInfo());
setDataAsStatus(error); setDataAsStatus(error);
maybePublishConfig(); maybePublishConfig();
} }
} }
@Override public String getEdsServiceName() {
public void onResourceDoesNotExist(String resourceName) { XdsClusterResource.CdsUpdate cdsUpdate = getData().getValue();
if (cancelled) { assert cdsUpdate.clusterType() == ClusterType.EDS;
return; String edsServiceName = cdsUpdate.edsServiceName();
if (edsServiceName == null) {
edsServiceName = cdsUpdate.clusterName();
} }
handleDoesNotExist(checkNotNull(resourceName, "resourceName")); return edsServiceName;
maybePublishConfig();
} }
} }
@ -868,15 +899,6 @@ final class XdsDependencyManager implements XdsConfig.XdsClusterSubscriptionRegi
maybePublishConfig(); maybePublishConfig();
} }
@Override
public void onResourceDoesNotExist(String resourceName) {
if (cancelled) {
return;
}
handleDoesNotExist(checkNotNull(resourceName, "resourceName"));
maybePublishConfig();
}
void addParentContext(CdsWatcher parentContext) { void addParentContext(CdsWatcher parentContext) {
parentContexts.add(checkNotNull(parentContext, "parentContext")); parentContexts.add(checkNotNull(parentContext, "parentContext"));
} }

View File

@ -45,6 +45,7 @@ import io.grpc.MetricRecorder;
import io.grpc.NameResolver; import io.grpc.NameResolver;
import io.grpc.Status; import io.grpc.Status;
import io.grpc.Status.Code; import io.grpc.Status.Code;
import io.grpc.StatusOr;
import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext;
import io.grpc.internal.GrpcUtil; import io.grpc.internal.GrpcUtil;
import io.grpc.internal.ObjectPool; import io.grpc.internal.ObjectPool;
@ -60,11 +61,9 @@ import io.grpc.xds.VirtualHost.Route.RouteAction.HashPolicy;
import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy; import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy;
import io.grpc.xds.VirtualHost.Route.RouteMatch; import io.grpc.xds.VirtualHost.Route.RouteMatch;
import io.grpc.xds.XdsNameResolverProvider.CallCounterProvider; import io.grpc.xds.XdsNameResolverProvider.CallCounterProvider;
import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate;
import io.grpc.xds.client.Bootstrapper.AuthorityInfo; import io.grpc.xds.client.Bootstrapper.AuthorityInfo;
import io.grpc.xds.client.Bootstrapper.BootstrapInfo; import io.grpc.xds.client.Bootstrapper.BootstrapInfo;
import io.grpc.xds.client.XdsClient; import io.grpc.xds.client.XdsClient;
import io.grpc.xds.client.XdsClient.ResourceWatcher;
import io.grpc.xds.client.XdsLogger; import io.grpc.xds.client.XdsLogger;
import io.grpc.xds.client.XdsLogger.XdsLogLevel; import io.grpc.xds.client.XdsLogger.XdsLogLevel;
import java.net.URI; import java.net.URI;
@ -127,6 +126,7 @@ final class XdsNameResolver extends NameResolver {
private final ConfigSelector configSelector = new ConfigSelector(); private final ConfigSelector configSelector = new ConfigSelector();
private final long randomChannelId; private final long randomChannelId;
private final MetricRecorder metricRecorder; private final MetricRecorder metricRecorder;
private final Args nameResolverArgs;
// Must be accessed in syncContext. // Must be accessed in syncContext.
// Filter instances are unique per channel, and per filter (name+typeUrl). // Filter instances are unique per channel, and per filter (name+typeUrl).
// NamedFilterConfig.filterStateKey -> filter_instance. // NamedFilterConfig.filterStateKey -> filter_instance.
@ -138,20 +138,17 @@ final class XdsNameResolver extends NameResolver {
private XdsClient xdsClient; private XdsClient xdsClient;
private CallCounterProvider callCounterProvider; private CallCounterProvider callCounterProvider;
private ResolveState resolveState; private ResolveState resolveState;
// Workaround for https://github.com/grpc/grpc-java/issues/8886 . This should be handled in
// XdsClient instead of here.
private boolean receivedConfig;
XdsNameResolver( XdsNameResolver(
URI targetUri, String name, @Nullable String overrideAuthority, URI targetUri, String name, @Nullable String overrideAuthority,
ServiceConfigParser serviceConfigParser, ServiceConfigParser serviceConfigParser,
SynchronizationContext syncContext, ScheduledExecutorService scheduler, SynchronizationContext syncContext, ScheduledExecutorService scheduler,
@Nullable Map<String, ?> bootstrapOverride, @Nullable Map<String, ?> bootstrapOverride,
MetricRecorder metricRecorder) { MetricRecorder metricRecorder, Args nameResolverArgs) {
this(targetUri, targetUri.getAuthority(), name, overrideAuthority, serviceConfigParser, this(targetUri, targetUri.getAuthority(), name, overrideAuthority, serviceConfigParser,
syncContext, scheduler, SharedXdsClientPoolProvider.getDefaultProvider(), syncContext, scheduler, SharedXdsClientPoolProvider.getDefaultProvider(),
ThreadSafeRandomImpl.instance, FilterRegistry.getDefaultRegistry(), bootstrapOverride, ThreadSafeRandomImpl.instance, FilterRegistry.getDefaultRegistry(), bootstrapOverride,
metricRecorder); metricRecorder, nameResolverArgs);
} }
@VisibleForTesting @VisibleForTesting
@ -161,7 +158,7 @@ final class XdsNameResolver extends NameResolver {
SynchronizationContext syncContext, ScheduledExecutorService scheduler, SynchronizationContext syncContext, ScheduledExecutorService scheduler,
XdsClientPoolFactory xdsClientPoolFactory, ThreadSafeRandom random, XdsClientPoolFactory xdsClientPoolFactory, ThreadSafeRandom random,
FilterRegistry filterRegistry, @Nullable Map<String, ?> bootstrapOverride, FilterRegistry filterRegistry, @Nullable Map<String, ?> bootstrapOverride,
MetricRecorder metricRecorder) { MetricRecorder metricRecorder, Args nameResolverArgs) {
this.targetAuthority = targetAuthority; this.targetAuthority = targetAuthority;
target = targetUri.toString(); target = targetUri.toString();
@ -180,6 +177,8 @@ final class XdsNameResolver extends NameResolver {
this.random = checkNotNull(random, "random"); this.random = checkNotNull(random, "random");
this.filterRegistry = checkNotNull(filterRegistry, "filterRegistry"); this.filterRegistry = checkNotNull(filterRegistry, "filterRegistry");
this.metricRecorder = metricRecorder; this.metricRecorder = metricRecorder;
this.nameResolverArgs = checkNotNull(nameResolverArgs, "nameResolverArgs");
randomChannelId = random.nextLong(); randomChannelId = random.nextLong();
logId = InternalLogId.allocate("xds-resolver", name); logId = InternalLogId.allocate("xds-resolver", name);
logger = XdsLogger.withLogId(logId); logger = XdsLogger.withLogId(logId);
@ -228,9 +227,8 @@ final class XdsNameResolver extends NameResolver {
} }
ldsResourceName = XdsClient.canonifyResourceName(ldsResourceName); ldsResourceName = XdsClient.canonifyResourceName(ldsResourceName);
callCounterProvider = SharedCallCounterMap.getInstance(); callCounterProvider = SharedCallCounterMap.getInstance();
resolveState = new ResolveState(ldsResourceName);
resolveState.start(); resolveState = new ResolveState(ldsResourceName); // auto starts
} }
private static String expandPercentS(String template, String replacement) { private static String expandPercentS(String template, String replacement) {
@ -241,7 +239,7 @@ final class XdsNameResolver extends NameResolver {
public void shutdown() { public void shutdown() {
logger.log(XdsLogLevel.INFO, "Shutdown"); logger.log(XdsLogLevel.INFO, "Shutdown");
if (resolveState != null) { if (resolveState != null) {
resolveState.stop(); resolveState.shutdown();
} }
if (xdsClient != null) { if (xdsClient != null) {
xdsClient = xdsClientPool.returnObject(xdsClient); xdsClient = xdsClientPool.returnObject(xdsClient);
@ -290,7 +288,7 @@ final class XdsNameResolver extends NameResolver {
} }
// called in syncContext // called in syncContext
private void updateResolutionResult() { private void updateResolutionResult(XdsConfig xdsConfig) {
syncContext.throwIfNotInThisSynchronizationContext(); syncContext.throwIfNotInThisSynchronizationContext();
ImmutableMap.Builder<String, Object> childPolicy = new ImmutableMap.Builder<>(); ImmutableMap.Builder<String, Object> childPolicy = new ImmutableMap.Builder<>();
@ -312,6 +310,8 @@ final class XdsNameResolver extends NameResolver {
Attributes attrs = Attributes attrs =
Attributes.newBuilder() Attributes.newBuilder()
.set(XdsAttributes.XDS_CLIENT_POOL, xdsClientPool) .set(XdsAttributes.XDS_CLIENT_POOL, xdsClientPool)
.set(XdsAttributes.XDS_CONFIG, xdsConfig)
.set(XdsAttributes.XDS_CLUSTER_SUBSCRIPT_REGISTRY, resolveState.xdsDependencyManager)
.set(XdsAttributes.CALL_COUNTER_PROVIDER, callCounterProvider) .set(XdsAttributes.CALL_COUNTER_PROVIDER, callCounterProvider)
.set(InternalConfigSelector.KEY, configSelector) .set(InternalConfigSelector.KEY, configSelector)
.build(); .build();
@ -321,7 +321,6 @@ final class XdsNameResolver extends NameResolver {
.setServiceConfig(parsedServiceConfig) .setServiceConfig(parsedServiceConfig)
.build(); .build();
listener.onResult2(result); listener.onResult2(result);
receivedConfig = true;
} }
/** /**
@ -540,7 +539,11 @@ final class XdsNameResolver extends NameResolver {
public void run() { public void run() {
if (clusterRefs.get(cluster).refCount.get() == 0) { if (clusterRefs.get(cluster).refCount.get() == 0) {
clusterRefs.remove(cluster); clusterRefs.remove(cluster);
updateResolutionResult(); if (resolveState.lastConfigOrStatus.hasValue()) {
updateResolutionResult(resolveState.lastConfigOrStatus.getValue());
} else {
resolveState.cleanUpRoutes(resolveState.lastConfigOrStatus.getStatus());
}
} }
} }
}); });
@ -629,82 +632,56 @@ final class XdsNameResolver extends NameResolver {
return "cluster_specifier_plugin:" + pluginName; return "cluster_specifier_plugin:" + pluginName;
} }
private class ResolveState implements ResourceWatcher<XdsListenerResource.LdsUpdate> { class ResolveState implements XdsDependencyManager.XdsConfigWatcher {
private final ConfigOrError emptyServiceConfig = private final ConfigOrError emptyServiceConfig =
serviceConfigParser.parseServiceConfig(Collections.<String, Object>emptyMap()); serviceConfigParser.parseServiceConfig(Collections.<String, Object>emptyMap());
private final String ldsResourceName; private final String authority;
private final XdsDependencyManager xdsDependencyManager;
private boolean stopped; private boolean stopped;
@Nullable @Nullable
private Set<String> existingClusters; // clusters to which new requests can be routed private Set<String> existingClusters; // clusters to which new requests can be routed
@Nullable private StatusOr<XdsConfig> lastConfigOrStatus;
private RouteDiscoveryState routeDiscoveryState;
ResolveState(String ldsResourceName) { private ResolveState(String ldsResourceName) {
this.ldsResourceName = ldsResourceName; authority = overrideAuthority != null ? overrideAuthority : encodedServiceAuthority;
xdsDependencyManager =
new XdsDependencyManager(xdsClient, this, syncContext, authority, ldsResourceName,
nameResolverArgs, scheduler);
} }
@Override private void shutdown() {
public void onChanged(final XdsListenerResource.LdsUpdate update) {
if (stopped) { if (stopped) {
return; return;
} }
logger.log(XdsLogLevel.INFO, "Receive LDS resource update: {0}", update);
HttpConnectionManager httpConnectionManager = update.httpConnectionManager(); stopped = true;
List<VirtualHost> virtualHosts = httpConnectionManager.virtualHosts(); xdsDependencyManager.shutdown();
String rdsName = httpConnectionManager.rdsName(); updateActiveFilters(null);
}
@Override
public void onUpdate(StatusOr<XdsConfig> updateOrStatus) {
if (stopped) {
return;
}
logger.log(XdsLogLevel.INFO, "Receive XDS resource update: {0}", updateOrStatus);
lastConfigOrStatus = updateOrStatus;
if (!updateOrStatus.hasValue()) {
updateActiveFilters(null);
cleanUpRoutes(updateOrStatus.getStatus());
return;
}
// Process Route
XdsConfig update = updateOrStatus.getValue();
HttpConnectionManager httpConnectionManager = update.getListener().httpConnectionManager();
VirtualHost virtualHost = update.getVirtualHost();
ImmutableList<NamedFilterConfig> filterConfigs = httpConnectionManager.httpFilterConfigs(); ImmutableList<NamedFilterConfig> filterConfigs = httpConnectionManager.httpFilterConfigs();
long streamDurationNano = httpConnectionManager.httpMaxStreamDurationNano(); long streamDurationNano = httpConnectionManager.httpMaxStreamDurationNano();
// Create/update HCM-bound state.
cleanUpRouteDiscoveryState();
updateActiveFilters(filterConfigs); updateActiveFilters(filterConfigs);
updateRoutes(update, virtualHost, streamDurationNano, filterConfigs);
// Routes specified directly in LDS.
if (virtualHosts != null) {
updateRoutes(virtualHosts, streamDurationNano, filterConfigs);
return;
}
// Routes provided by RDS.
routeDiscoveryState = new RouteDiscoveryState(rdsName, streamDurationNano, filterConfigs);
logger.log(XdsLogLevel.INFO, "Start watching RDS resource {0}", rdsName);
xdsClient.watchXdsResource(XdsRouteConfigureResource.getInstance(),
rdsName, routeDiscoveryState, syncContext);
}
@Override
public void onError(final Status error) {
if (stopped || receivedConfig) {
return;
}
listener.onError(Status.UNAVAILABLE.withCause(error.getCause()).withDescription(
String.format("Unable to load LDS %s. xDS server returned: %s: %s",
ldsResourceName, error.getCode(), error.getDescription())));
}
@Override
public void onResourceDoesNotExist(final String resourceName) {
if (stopped) {
return;
}
String error = "LDS resource does not exist: " + resourceName;
logger.log(XdsLogLevel.INFO, error);
cleanUpRouteDiscoveryState();
updateActiveFilters(null);
cleanUpRoutes(error);
}
private void start() {
logger.log(XdsLogLevel.INFO, "Start watching LDS resource {0}", ldsResourceName);
xdsClient.watchXdsResource(XdsListenerResource.getInstance(),
ldsResourceName, this, syncContext);
}
private void stop() {
logger.log(XdsLogLevel.INFO, "Stop watching LDS resource {0}", ldsResourceName);
stopped = true;
cleanUpRouteDiscoveryState();
updateActiveFilters(null);
xdsClient.cancelXdsResourceWatch(XdsListenerResource.getInstance(), ldsResourceName, this);
} }
// called in syncContext // called in syncContext
@ -732,18 +709,11 @@ final class XdsNameResolver extends NameResolver {
} }
} }
// called in syncContext private void updateRoutes(
private void updateRoutes(List<VirtualHost> virtualHosts, long httpMaxStreamDurationNano, XdsConfig xdsConfig,
@Nullable VirtualHost virtualHost,
long httpMaxStreamDurationNano,
@Nullable List<NamedFilterConfig> filterConfigs) { @Nullable List<NamedFilterConfig> filterConfigs) {
String authority = overrideAuthority != null ? overrideAuthority : encodedServiceAuthority;
VirtualHost virtualHost = RoutingUtils.findVirtualHostForHostName(virtualHosts, authority);
if (virtualHost == null) {
String error = "Failed to find virtual host matching hostname: " + authority;
logger.log(XdsLogLevel.WARNING, error);
cleanUpRoutes(error);
return;
}
List<Route> routes = virtualHost.routes(); List<Route> routes = virtualHost.routes();
ImmutableList.Builder<RouteData> routesData = ImmutableList.builder(); ImmutableList.Builder<RouteData> routesData = ImmutableList.builder();
@ -826,7 +796,7 @@ final class XdsNameResolver extends NameResolver {
} }
// Update service config to include newly added clusters. // Update service config to include newly added clusters.
if (shouldUpdateResult && routingConfig != null) { if (shouldUpdateResult && routingConfig != null) {
updateResolutionResult(); updateResolutionResult(xdsConfig);
shouldUpdateResult = false; shouldUpdateResult = false;
} }
// Make newly added clusters selectable by config selector and deleted clusters no longer // Make newly added clusters selectable by config selector and deleted clusters no longer
@ -840,7 +810,7 @@ final class XdsNameResolver extends NameResolver {
} }
} }
if (shouldUpdateResult) { if (shouldUpdateResult) {
updateResolutionResult(); updateResolutionResult(xdsConfig);
} }
} }
@ -882,10 +852,8 @@ final class XdsNameResolver extends NameResolver {
return combineInterceptors(filterInterceptors.build()); return combineInterceptors(filterInterceptors.build());
} }
private void cleanUpRoutes(String error) { private void cleanUpRoutes(Status error) {
String errorWithNodeId = routingConfig = new RoutingConfig(error);
error + ", xDS node ID: " + xdsClient.getBootstrapInfo().node().getId();
routingConfig = new RoutingConfig(Status.UNAVAILABLE.withDescription(errorWithNodeId));
if (existingClusters != null) { if (existingClusters != null) {
for (String cluster : existingClusters) { for (String cluster : existingClusters) {
int count = clusterRefs.get(cluster).refCount.decrementAndGet(); int count = clusterRefs.get(cluster).refCount.decrementAndGet();
@ -904,64 +872,6 @@ final class XdsNameResolver extends NameResolver {
.build()) .build())
.setServiceConfig(emptyServiceConfig) .setServiceConfig(emptyServiceConfig)
.build()); .build());
receivedConfig = true;
}
private void cleanUpRouteDiscoveryState() {
if (routeDiscoveryState != null) {
String rdsName = routeDiscoveryState.resourceName;
logger.log(XdsLogLevel.INFO, "Stop watching RDS resource {0}", rdsName);
xdsClient.cancelXdsResourceWatch(XdsRouteConfigureResource.getInstance(), rdsName,
routeDiscoveryState);
routeDiscoveryState = null;
}
}
/**
* Discovery state for RouteConfiguration resource. One instance for each Listener resource
* update.
*/
private class RouteDiscoveryState implements ResourceWatcher<RdsUpdate> {
private final String resourceName;
private final long httpMaxStreamDurationNano;
@Nullable
private final List<NamedFilterConfig> filterConfigs;
private RouteDiscoveryState(String resourceName, long httpMaxStreamDurationNano,
@Nullable List<NamedFilterConfig> filterConfigs) {
this.resourceName = resourceName;
this.httpMaxStreamDurationNano = httpMaxStreamDurationNano;
this.filterConfigs = filterConfigs;
}
@Override
public void onChanged(final RdsUpdate update) {
if (RouteDiscoveryState.this != routeDiscoveryState) {
return;
}
logger.log(XdsLogLevel.INFO, "Received RDS resource update: {0}", update);
updateRoutes(update.virtualHosts, httpMaxStreamDurationNano, filterConfigs);
}
@Override
public void onError(final Status error) {
if (RouteDiscoveryState.this != routeDiscoveryState || receivedConfig) {
return;
}
listener.onError(Status.UNAVAILABLE.withCause(error.getCause()).withDescription(
String.format("Unable to load RDS %s. xDS server returned: %s: %s",
resourceName, error.getCode(), error.getDescription())));
}
@Override
public void onResourceDoesNotExist(final String resourceName) {
if (RouteDiscoveryState.this != routeDiscoveryState) {
return;
}
String error = "RDS resource does not exist: " + resourceName;
logger.log(XdsLogLevel.INFO, error);
cleanUpRoutes(error);
}
} }
} }

View File

@ -82,7 +82,7 @@ public final class XdsNameResolverProvider extends NameResolverProvider {
args.getServiceConfigParser(), args.getSynchronizationContext(), args.getServiceConfigParser(), args.getSynchronizationContext(),
args.getScheduledExecutorService(), args.getScheduledExecutorService(),
bootstrapOverride, bootstrapOverride,
args.getMetricRecorder()); args.getMetricRecorder(), args);
} }
return null; return null;
} }

View File

@ -17,6 +17,7 @@
package io.grpc.xds; package io.grpc.xds;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static io.grpc.StatusMatcher.statusHasCode;
import static io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType.AGGREGATE; import static io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType.AGGREGATE;
import static io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType.EDS; import static io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType.EDS;
import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_CDS; import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_CDS;
@ -32,7 +33,6 @@ import static io.grpc.xds.client.CommonBootstrapperTestUtils.SERVER_URI;
import static org.mockito.AdditionalAnswers.delegatesTo; import static org.mockito.AdditionalAnswers.delegatesTo;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
@ -47,24 +47,26 @@ import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment;
import io.envoyproxy.envoy.config.listener.v3.Listener; import io.envoyproxy.envoy.config.listener.v3.Listener;
import io.envoyproxy.envoy.config.route.v3.RouteConfiguration; import io.envoyproxy.envoy.config.route.v3.RouteConfiguration;
import io.grpc.BindableService; import io.grpc.BindableService;
import io.grpc.ChannelLogger;
import io.grpc.ManagedChannel; import io.grpc.ManagedChannel;
import io.grpc.NameResolver;
import io.grpc.Server; import io.grpc.Server;
import io.grpc.Status; import io.grpc.Status;
import io.grpc.StatusOr; import io.grpc.StatusOr;
import io.grpc.StatusOrMatcher;
import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext;
import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.internal.ExponentialBackoffPolicy; import io.grpc.internal.ExponentialBackoffPolicy;
import io.grpc.internal.FakeClock; import io.grpc.internal.FakeClock;
import io.grpc.internal.GrpcUtil;
import io.grpc.testing.GrpcCleanupRule; import io.grpc.testing.GrpcCleanupRule;
import io.grpc.xds.XdsClusterResource.CdsUpdate;
import io.grpc.xds.XdsConfig.XdsClusterConfig; import io.grpc.xds.XdsConfig.XdsClusterConfig;
import io.grpc.xds.XdsEndpointResource.EdsUpdate; import io.grpc.xds.XdsEndpointResource.EdsUpdate;
import io.grpc.xds.XdsListenerResource.LdsUpdate;
import io.grpc.xds.client.CommonBootstrapperTestUtils; import io.grpc.xds.client.CommonBootstrapperTestUtils;
import io.grpc.xds.client.XdsClient;
import io.grpc.xds.client.XdsClientImpl; import io.grpc.xds.client.XdsClientImpl;
import io.grpc.xds.client.XdsClientMetricReporter; import io.grpc.xds.client.XdsClientMetricReporter;
import io.grpc.xds.client.XdsResourceType;
import io.grpc.xds.client.XdsTransportFactory; import io.grpc.xds.client.XdsTransportFactory;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
@ -77,7 +79,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Queue; import java.util.Queue;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -108,7 +110,9 @@ public class XdsDependencyManagerTest {
private XdsClientMetricReporter xdsClientMetricReporter; private XdsClientMetricReporter xdsClientMetricReporter;
private final SynchronizationContext syncContext = private final SynchronizationContext syncContext =
new SynchronizationContext(mock(Thread.UncaughtExceptionHandler.class)); new SynchronizationContext((t, e) -> {
throw new AssertionError(e);
});
private ManagedChannel channel; private ManagedChannel channel;
private XdsClientImpl xdsClient; private XdsClientImpl xdsClient;
@ -133,9 +137,17 @@ public class XdsDependencyManagerTest {
private XdsConfig defaultXdsConfig; // set in setUp() private XdsConfig defaultXdsConfig; // set in setUp()
@Captor @Captor
private ArgumentCaptor<XdsConfig> xdsConfigCaptor; private ArgumentCaptor<StatusOr<XdsConfig>> xdsUpdateCaptor;
@Captor private final NameResolver.Args nameResolverArgs = NameResolver.Args.newBuilder()
private ArgumentCaptor<Status> statusCaptor; .setDefaultPort(8080)
.setProxyDetector(GrpcUtil.DEFAULT_PROXY_DETECTOR)
.setSynchronizationContext(syncContext)
.setServiceConfigParser(mock(NameResolver.ServiceConfigParser.class))
.setChannelLogger(mock(ChannelLogger.class))
.setScheduledExecutorService(fakeClock.getScheduledExecutorService())
.build();
private final ScheduledExecutorService scheduler = fakeClock.getScheduledExecutorService();
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
@ -180,36 +192,36 @@ public class XdsDependencyManagerTest {
@Test @Test
public void verify_basic_config() { public void verify_basic_config() {
xdsDependencyManager = new XdsDependencyManager( xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); serverName, serverName, nameResolverArgs, scheduler);
verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig));
testWatcher.verifyStats(1, 0, 0); testWatcher.verifyStats(1, 0);
} }
@Test @Test
public void verify_config_update() { public void verify_config_update() {
xdsDependencyManager = new XdsDependencyManager( xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); serverName, serverName, nameResolverArgs, scheduler);
InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); InOrder inOrder = Mockito.inOrder(xdsConfigWatcher);
inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig));
testWatcher.verifyStats(1, 0, 0); testWatcher.verifyStats(1, 0);
assertThat(testWatcher.lastConfig).isEqualTo(defaultXdsConfig); assertThat(testWatcher.lastConfig).isEqualTo(defaultXdsConfig);
XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS2", "CDS2", "EDS2", XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS2", "CDS2", "EDS2",
ENDPOINT_HOSTNAME + "2", ENDPOINT_PORT + 2); ENDPOINT_HOSTNAME + "2", ENDPOINT_PORT + 2);
inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(ArgumentMatchers.notNull()); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(ArgumentMatchers.notNull());
testWatcher.verifyStats(2, 0, 0); testWatcher.verifyStats(2, 0);
assertThat(testWatcher.lastConfig).isNotEqualTo(defaultXdsConfig); assertThat(testWatcher.lastConfig).isNotEqualTo(defaultXdsConfig);
} }
@Test @Test
public void verify_simple_aggregate() { public void verify_simple_aggregate() {
InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); InOrder inOrder = Mockito.inOrder(xdsConfigWatcher);
xdsDependencyManager = new XdsDependencyManager( xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); serverName, serverName, nameResolverArgs, scheduler);
inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig));
List<String> childNames = Arrays.asList("clusterC", "clusterB", "clusterA"); List<String> childNames = Arrays.asList("clusterC", "clusterB", "clusterA");
String rootName = "root_c"; String rootName = "root_c";
@ -226,14 +238,14 @@ public class XdsDependencyManagerTest {
testWatcher.lastConfig.getClusters(); testWatcher.lastConfig.getClusters();
assertThat(lastConfigClusters).hasSize(childNames.size() + 1); assertThat(lastConfigClusters).hasSize(childNames.size() + 1);
StatusOr<XdsClusterConfig> rootC = lastConfigClusters.get(rootName); StatusOr<XdsClusterConfig> rootC = lastConfigClusters.get(rootName);
XdsClusterResource.CdsUpdate rootUpdate = rootC.getValue().getClusterResource(); CdsUpdate rootUpdate = rootC.getValue().getClusterResource();
assertThat(rootUpdate.clusterType()).isEqualTo(AGGREGATE); assertThat(rootUpdate.clusterType()).isEqualTo(AGGREGATE);
assertThat(rootUpdate.prioritizedClusterNames()).isEqualTo(childNames); assertThat(rootUpdate.prioritizedClusterNames()).isEqualTo(childNames);
for (String childName : childNames) { for (String childName : childNames) {
assertThat(lastConfigClusters).containsKey(childName); assertThat(lastConfigClusters).containsKey(childName);
StatusOr<XdsClusterConfig> childConfigOr = lastConfigClusters.get(childName); StatusOr<XdsClusterConfig> childConfigOr = lastConfigClusters.get(childName);
XdsClusterResource.CdsUpdate childResource = CdsUpdate childResource =
childConfigOr.getValue().getClusterResource(); childConfigOr.getValue().getClusterResource();
assertThat(childResource.clusterType()).isEqualTo(EDS); assertThat(childResource.clusterType()).isEqualTo(EDS);
assertThat(childResource.edsServiceName()).isEqualTo(getEdsNameForCluster(childName)); assertThat(childResource.edsServiceName()).isEqualTo(getEdsNameForCluster(childName));
@ -266,54 +278,57 @@ public class XdsDependencyManagerTest {
List<String> childNames2 = Arrays.asList("clusterA", "clusterX"); List<String> childNames2 = Arrays.asList("clusterA", "clusterX");
XdsTestUtils.addAggregateToExistingConfig(controlPlaneService, rootName2, childNames2); XdsTestUtils.addAggregateToExistingConfig(controlPlaneService, rootName2, childNames2);
xdsDependencyManager = new XdsDependencyManager( xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); serverName, serverName, nameResolverArgs, scheduler);
inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(any());
Closeable subscription1 = xdsDependencyManager.subscribeToCluster(rootName1); Closeable subscription1 = xdsDependencyManager.subscribeToCluster(rootName1);
inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(any()); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(any());
Closeable subscription2 = xdsDependencyManager.subscribeToCluster(rootName2); Closeable subscription2 = xdsDependencyManager.subscribeToCluster(rootName2);
inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture());
testWatcher.verifyStats(3, 0, 0); testWatcher.verifyStats(3, 0);
ImmutableSet.Builder<String> builder = ImmutableSet.builder(); ImmutableSet.Builder<String> builder = ImmutableSet.builder();
Set<String> expectedClusters = builder.add(rootName1).add(rootName2).add(CLUSTER_NAME) Set<String> expectedClusters = builder.add(rootName1).add(rootName2).add(CLUSTER_NAME)
.addAll(childNames).addAll(childNames2).build(); .addAll(childNames).addAll(childNames2).build();
assertThat(xdsConfigCaptor.getValue().getClusters().keySet()).isEqualTo(expectedClusters); assertThat(xdsUpdateCaptor.getValue().getValue().getClusters().keySet())
.isEqualTo(expectedClusters);
// Close 1 subscription shouldn't affect the other or RDS subscriptions // Close 1 subscription shouldn't affect the other or RDS subscriptions
subscription1.close(); subscription1.close();
inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture());
builder = ImmutableSet.builder(); builder = ImmutableSet.builder();
Set<String> expectedClusters2 = Set<String> expectedClusters2 =
builder.add(rootName2).add(CLUSTER_NAME).addAll(childNames2).build(); builder.add(rootName2).add(CLUSTER_NAME).addAll(childNames2).build();
assertThat(xdsConfigCaptor.getValue().getClusters().keySet()).isEqualTo(expectedClusters2); assertThat(xdsUpdateCaptor.getValue().getValue().getClusters().keySet())
.isEqualTo(expectedClusters2);
subscription2.close(); subscription2.close();
inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig));
} }
@Test @Test
public void testDelayedSubscription() { public void testDelayedSubscription() {
InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); InOrder inOrder = Mockito.inOrder(xdsConfigWatcher);
xdsDependencyManager = new XdsDependencyManager( xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); serverName, serverName, nameResolverArgs, scheduler);
inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig));
String rootName1 = "root_c"; String rootName1 = "root_c";
Closeable subscription1 = xdsDependencyManager.subscribeToCluster(rootName1); Closeable subscription1 = xdsDependencyManager.subscribeToCluster(rootName1);
assertThat(subscription1).isNotNull(); assertThat(subscription1).isNotNull();
fakeClock.forwardTime(16, TimeUnit.SECONDS); fakeClock.forwardTime(16, TimeUnit.SECONDS);
inOrder.verify(xdsConfigWatcher).onUpdate(xdsConfigCaptor.capture()); inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture());
assertThat(xdsConfigCaptor.getValue().getClusters().get(rootName1).toString()).isEqualTo( Status status = xdsUpdateCaptor.getValue().getValue().getClusters().get(rootName1).getStatus();
StatusOr.fromStatus(Status.UNAVAILABLE.withDescription( assertThat(status.getCode()).isEqualTo(Status.Code.UNAVAILABLE);
"No " + toContextStr(CLUSTER_TYPE_NAME, rootName1))).toString()); assertThat(status.getDescription()).contains(rootName1);
List<String> childNames = Arrays.asList("clusterC", "clusterB", "clusterA"); List<String> childNames = Arrays.asList("clusterC", "clusterB", "clusterA");
XdsTestUtils.addAggregateToExistingConfig(controlPlaneService, rootName1, childNames); XdsTestUtils.addAggregateToExistingConfig(controlPlaneService, rootName1, childNames);
inOrder.verify(xdsConfigWatcher).onUpdate(xdsConfigCaptor.capture()); inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture());
assertThat(xdsConfigCaptor.getValue().getClusters().get(rootName1).hasValue()).isTrue(); assertThat(xdsUpdateCaptor.getValue().getValue().getClusters().get(rootName1).hasValue())
.isTrue();
} }
@Test @Test
@ -342,109 +357,99 @@ public class XdsDependencyManagerTest {
edsMap.put("garbageEds", clusterLoadAssignment); edsMap.put("garbageEds", clusterLoadAssignment);
controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap);
xdsDependencyManager = new XdsDependencyManager( xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); serverName, serverName, nameResolverArgs, scheduler);
fakeClock.forwardTime(16, TimeUnit.SECONDS); fakeClock.forwardTime(16, TimeUnit.SECONDS);
verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture());
List<StatusOr<XdsClusterConfig>> returnedClusters = new ArrayList<>(); List<StatusOr<XdsClusterConfig>> returnedClusters = new ArrayList<>();
for (String childName : childNames) { for (String childName : childNames) {
returnedClusters.add(xdsConfigCaptor.getValue().getClusters().get(childName)); returnedClusters.add(xdsUpdateCaptor.getValue().getValue().getClusters().get(childName));
} }
// Check that missing cluster reported Status and the other 2 are present // Check that missing cluster reported Status and the other 2 are present
Status expectedClusterStatus = Status.UNAVAILABLE.withDescription(
"No " + toContextStr(CLUSTER_TYPE_NAME, childNames.get(2)));
StatusOr<XdsClusterConfig> missingCluster = returnedClusters.get(2); StatusOr<XdsClusterConfig> missingCluster = returnedClusters.get(2);
assertThat(missingCluster.getStatus().toString()).isEqualTo(expectedClusterStatus.toString()); assertThat(missingCluster.getStatus().getCode()).isEqualTo(Status.Code.UNAVAILABLE);
assertThat(missingCluster.getStatus().getDescription()).contains(childNames.get(2));
assertThat(returnedClusters.get(0).hasValue()).isTrue(); assertThat(returnedClusters.get(0).hasValue()).isTrue();
assertThat(returnedClusters.get(1).hasValue()).isTrue(); assertThat(returnedClusters.get(1).hasValue()).isTrue();
// Check that missing EDS reported Status, the other one is present and the garbage EDS is not // Check that missing EDS reported Status, the other one is present and the garbage EDS is not
Status expectedEdsStatus = Status.UNAVAILABLE.withDescription(
"No " + toContextStr(ENDPOINT_TYPE_NAME, XdsTestUtils.EDS_NAME + 1));
assertThat(getEndpoint(returnedClusters.get(0)).hasValue()).isTrue(); assertThat(getEndpoint(returnedClusters.get(0)).hasValue()).isTrue();
assertThat(getEndpoint(returnedClusters.get(1)).hasValue()).isFalse(); assertThat(getEndpoint(returnedClusters.get(1)).getStatus().getCode())
assertThat(getEndpoint(returnedClusters.get(1)).getStatus().toString()) .isEqualTo(Status.Code.UNAVAILABLE);
.isEqualTo(expectedEdsStatus.toString()); assertThat(getEndpoint(returnedClusters.get(1)).getStatus().getDescription())
.contains(XdsTestUtils.EDS_NAME + 1);
verify(xdsConfigWatcher, never()).onResourceDoesNotExist(any()); verify(xdsConfigWatcher, never()).onUpdate(
testWatcher.verifyStats(1, 0, 0); argThat(StatusOrMatcher.hasStatus(statusHasCode(Status.Code.UNAVAILABLE))));
testWatcher.verifyStats(1, 0);
} }
@Test @Test
public void testMissingLds() { public void testMissingLds() {
xdsDependencyManager = new XdsDependencyManager( String ldsName = "badLdsName";
xdsClient, xdsConfigWatcher, syncContext, serverName, "badLdsName"); xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, ldsName, nameResolverArgs, scheduler);
fakeClock.forwardTime(16, TimeUnit.SECONDS); fakeClock.forwardTime(16, TimeUnit.SECONDS);
verify(xdsConfigWatcher, timeout(1000)).onResourceDoesNotExist( verify(xdsConfigWatcher, timeout(1000)).onUpdate(
toContextStr(XdsListenerResource.getInstance().typeName(), "badLdsName")); argThat(StatusOrMatcher.hasStatus(statusHasCode(Status.Code.UNAVAILABLE)
.andDescriptionContains(ldsName))));
testWatcher.verifyStats(0, 0, 1); testWatcher.verifyStats(0, 1);
}
@Test
public void testTcpListenerErrors() {
Listener serverListener =
ControlPlaneRule.buildServerListener().toBuilder().setName(serverName).build();
controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, ImmutableMap.of(serverName, serverListener));
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
fakeClock.forwardTime(16, TimeUnit.SECONDS);
verify(xdsConfigWatcher, timeout(1000)).onUpdate(
argThat(StatusOrMatcher.hasStatus(
statusHasCode(Status.Code.UNAVAILABLE).andDescriptionContains("Not an API listener"))));
testWatcher.verifyStats(0, 1);
} }
@Test @Test
public void testMissingRds() { public void testMissingRds() {
Listener serverListener = ControlPlaneRule.buildServerListener(); String rdsName = "badRdsName";
Listener clientListener = Listener clientListener = ControlPlaneRule.buildClientListener(serverName, serverName, rdsName);
ControlPlaneRule.buildClientListener(serverName, serverName, "badRdsName");
controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS,
ImmutableMap.of(XdsTestUtils.SERVER_LISTENER, serverListener, serverName, clientListener)); ImmutableMap.of(serverName, clientListener));
xdsDependencyManager = new XdsDependencyManager( xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); serverName, serverName, nameResolverArgs, scheduler);
fakeClock.forwardTime(16, TimeUnit.SECONDS); fakeClock.forwardTime(16, TimeUnit.SECONDS);
verify(xdsConfigWatcher, timeout(1000)).onResourceDoesNotExist( verify(xdsConfigWatcher, timeout(1000)).onUpdate(
toContextStr(XdsRouteConfigureResource.getInstance().typeName(), "badRdsName")); argThat(StatusOrMatcher.hasStatus(statusHasCode(Status.Code.UNAVAILABLE)
.andDescriptionContains(rdsName))));
testWatcher.verifyStats(0, 0, 1); testWatcher.verifyStats(0, 1);
} }
@Test @Test
public void testUpdateToMissingVirtualHost() { public void testUpdateToMissingVirtualHost() {
InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); RouteConfiguration routeConfig = XdsTestUtils.buildRouteConfiguration(
WrappedXdsClient wrappedXdsClient = new WrappedXdsClient(xdsClient, syncContext); "wrong-virtual-host", XdsTestUtils.RDS_NAME, XdsTestUtils.CLUSTER_NAME);
xdsDependencyManager = new XdsDependencyManager( controlPlaneService.setXdsConfig(
wrappedXdsClient, xdsConfigWatcher, syncContext, serverName, serverName); ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, routeConfig));
inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig); xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
// Update with a config that has a virtual host that doesn't match the server name // Update with a config that has a virtual host that doesn't match the server name
wrappedXdsClient.deliverLdsUpdate(0L, buildUnmatchedVirtualHosts()); verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture());
inOrder.verify(xdsConfigWatcher, timeout(1000)).onError(any(), statusCaptor.capture()); assertThat(xdsUpdateCaptor.getValue().getStatus().getDescription())
assertThat(statusCaptor.getValue().getDescription()) .contains("Failed to find virtual host matching hostname: " + serverName);
.isEqualTo("Failed to find virtual host matching hostname: " + serverName);
testWatcher.verifyStats(1, 1, 0); testWatcher.verifyStats(0, 1);
wrappedXdsClient.shutdown();
}
private List<io.grpc.xds.VirtualHost> buildUnmatchedVirtualHosts() {
io.grpc.xds.VirtualHost.Route route1 =
io.grpc.xds.VirtualHost.Route.forAction(
io.grpc.xds.VirtualHost.Route.RouteMatch.withPathExactOnly("/GreetService/bye"),
io.grpc.xds.VirtualHost.Route.RouteAction.forCluster(
"cluster-bar.googleapis.com", Collections.emptyList(),
TimeUnit.SECONDS.toNanos(15L), null, false), ImmutableMap.of());
io.grpc.xds.VirtualHost.Route route2 =
io.grpc.xds.VirtualHost.Route.forAction(
io.grpc.xds.VirtualHost.Route.RouteMatch.withPathExactOnly("/HelloService/hi"),
io.grpc.xds.VirtualHost.Route.RouteAction.forCluster(
"cluster-foo.googleapis.com", Collections.emptyList(),
TimeUnit.SECONDS.toNanos(15L), null, false),
ImmutableMap.of());
return Arrays.asList(
io.grpc.xds.VirtualHost.create("virtualhost-foo", Collections.singletonList("hello"
+ ".googleapis.com"),
Collections.singletonList(route1),
ImmutableMap.of()),
io.grpc.xds.VirtualHost.create("virtualhost-bar", Collections.singletonList("hi"
+ ".googleapis.com"),
Collections.singletonList(route2),
ImmutableMap.of()));
} }
@Test @Test
@ -452,37 +457,32 @@ public class XdsDependencyManagerTest {
String ldsResourceName = String ldsResourceName =
"xdstp://unknown.example.com/envoy.config.listener.v3.Listener/listener1"; "xdstp://unknown.example.com/envoy.config.listener.v3.Listener/listener1";
xdsDependencyManager = new XdsDependencyManager( xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
xdsClient, xdsConfigWatcher, syncContext, serverName, ldsResourceName); serverName, ldsResourceName, nameResolverArgs, scheduler);
Status expectedStatus = Status.INVALID_ARGUMENT.withDescription( verify(xdsConfigWatcher, timeout(1000)).onUpdate(
"Wrong configuration: xds server does not exist for resource " + ldsResourceName); argThat(StatusOrMatcher.hasStatus(
String context = toContextStr(XdsListenerResource.getInstance().typeName(), ldsResourceName); statusHasCode(Status.Code.UNAVAILABLE).andDescriptionContains(ldsResourceName))));
verify(xdsConfigWatcher, timeout(1000))
.onError(eq(context), argThat(new XdsTestUtils.StatusMatcher(expectedStatus)));
fakeClock.forwardTime(16, TimeUnit.SECONDS); fakeClock.forwardTime(16, TimeUnit.SECONDS);
testWatcher.verifyStats(0, 1, 0); testWatcher.verifyStats(0, 1);
} }
@Test @Test
public void testChangeRdsName_fromLds() { public void testChangeRdsName_fromLds() {
// TODO implement
InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); InOrder inOrder = Mockito.inOrder(xdsConfigWatcher);
Listener serverListener = ControlPlaneRule.buildServerListener(); xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
xdsDependencyManager = new XdsDependencyManager( inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(StatusOr.fromValue(defaultXdsConfig));
xdsClient, xdsConfigWatcher, syncContext, serverName, serverName);
inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(defaultXdsConfig);
String newRdsName = "newRdsName1"; String newRdsName = "newRdsName1";
Listener clientListener = buildInlineClientListener(newRdsName, CLUSTER_NAME); Listener clientListener = buildInlineClientListener(newRdsName, CLUSTER_NAME);
controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS,
ImmutableMap.of(XdsTestUtils.SERVER_LISTENER, serverListener, serverName, clientListener)); ImmutableMap.of(serverName, clientListener));
inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture());
assertThat(xdsConfigCaptor.getValue()).isNotEqualTo(defaultXdsConfig); assertThat(xdsUpdateCaptor.getValue().getValue()).isNotEqualTo(defaultXdsConfig);
assertThat(xdsConfigCaptor.getValue().getVirtualHost().name()).isEqualTo(newRdsName); assertThat(xdsUpdateCaptor.getValue().getValue().getVirtualHost().name()).isEqualTo(newRdsName);
} }
@Test @Test
@ -527,22 +527,22 @@ public class XdsDependencyManagerTest {
// Start the actual test // Start the actual test
InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); InOrder inOrder = Mockito.inOrder(xdsConfigWatcher);
xdsDependencyManager = new XdsDependencyManager( xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); serverName, serverName, nameResolverArgs, scheduler);
inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture());
XdsConfig initialConfig = xdsConfigCaptor.getValue(); XdsConfig initialConfig = xdsUpdateCaptor.getValue().getValue();
// Make sure that adding subscriptions that rds points at doesn't change the config // Make sure that adding subscriptions that rds points at doesn't change the config
Closeable rootSub = xdsDependencyManager.subscribeToCluster("root"); Closeable rootSub = xdsDependencyManager.subscribeToCluster("root");
assertThat(xdsDependencyManager.buildConfig()).isEqualTo(initialConfig); assertThat(xdsDependencyManager.buildUpdate().getValue()).isEqualTo(initialConfig);
Closeable clusterAB11Sub = xdsDependencyManager.subscribeToCluster("clusterAB11"); Closeable clusterAB11Sub = xdsDependencyManager.subscribeToCluster("clusterAB11");
assertThat(xdsDependencyManager.buildConfig()).isEqualTo(initialConfig); assertThat(xdsDependencyManager.buildUpdate().getValue()).isEqualTo(initialConfig);
// Make sure that closing subscriptions that rds points at doesn't change the config // Make sure that closing subscriptions that rds points at doesn't change the config
rootSub.close(); rootSub.close();
assertThat(xdsDependencyManager.buildConfig()).isEqualTo(initialConfig); assertThat(xdsDependencyManager.buildUpdate().getValue()).isEqualTo(initialConfig);
clusterAB11Sub.close(); clusterAB11Sub.close();
assertThat(xdsDependencyManager.buildConfig()).isEqualTo(initialConfig); assertThat(xdsDependencyManager.buildUpdate().getValue()).isEqualTo(initialConfig);
// Make an explicit root subscription and then change RDS to point to A11 // Make an explicit root subscription and then change RDS to point to A11
rootSub = xdsDependencyManager.subscribeToCluster("root"); rootSub = xdsDependencyManager.subscribeToCluster("root");
@ -550,13 +550,14 @@ public class XdsDependencyManagerTest {
XdsTestUtils.buildRouteConfiguration(serverName, XdsTestUtils.RDS_NAME, "clusterA11"); XdsTestUtils.buildRouteConfiguration(serverName, XdsTestUtils.RDS_NAME, "clusterA11");
controlPlaneService.setXdsConfig( controlPlaneService.setXdsConfig(
ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, newRouteConfig)); ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, newRouteConfig));
inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture());
assertThat(xdsConfigCaptor.getValue().getClusters().keySet().size()).isEqualTo(4); assertThat(xdsUpdateCaptor.getValue().getValue().getClusters().keySet().size()).isEqualTo(4);
// Now that it is released, we should only have A11 // Now that it is released, we should only have A11
rootSub.close(); rootSub.close();
inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture());
assertThat(xdsConfigCaptor.getValue().getClusters().keySet()).containsExactly("clusterA11"); assertThat(xdsUpdateCaptor.getValue().getValue().getClusters().keySet())
.containsExactly("clusterA11");
} }
@Test @Test
@ -587,10 +588,10 @@ public class XdsDependencyManagerTest {
controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap);
// Start the actual test // Start the actual test
xdsDependencyManager = new XdsDependencyManager( xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); serverName, serverName, nameResolverArgs, scheduler);
verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture());
XdsConfig initialConfig = xdsConfigCaptor.getValue(); XdsConfig initialConfig = xdsUpdateCaptor.getValue().getValue();
assertThat(initialConfig.getClusters().keySet()) assertThat(initialConfig.getClusters().keySet())
.containsExactly("root", "clusterA", "clusterB"); .containsExactly("root", "clusterA", "clusterB");
@ -605,8 +606,8 @@ public class XdsDependencyManagerTest {
@Test @Test
public void testChangeRdsName_FromLds_complexTree() { public void testChangeRdsName_FromLds_complexTree() {
xdsDependencyManager = new XdsDependencyManager( xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); serverName, serverName, nameResolverArgs, scheduler);
// Create the same tree as in testMultipleParentsInCdsTree // Create the same tree as in testMultipleParentsInCdsTree
Cluster rootCluster = Cluster rootCluster =
@ -639,11 +640,10 @@ public class XdsDependencyManagerTest {
// Do the test // Do the test
String newRdsName = "newRdsName1"; String newRdsName = "newRdsName1";
Listener clientListener = buildInlineClientListener(newRdsName, "root"); Listener clientListener = buildInlineClientListener(newRdsName, "root");
Listener serverListener = ControlPlaneRule.buildServerListener();
controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS,
ImmutableMap.of(XdsTestUtils.SERVER_LISTENER, serverListener, serverName, clientListener)); ImmutableMap.of(serverName, clientListener));
inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsConfigCaptor.capture()); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture());
XdsConfig config = xdsConfigCaptor.getValue(); XdsConfig config = xdsUpdateCaptor.getValue().getValue();
assertThat(config.getVirtualHost().name()).isEqualTo(newRdsName); assertThat(config.getVirtualHost().name()).isEqualTo(newRdsName);
assertThat(config.getClusters().size()).isEqualTo(4); assertThat(config.getClusters().size()).isEqualTo(4);
} }
@ -652,9 +652,9 @@ public class XdsDependencyManagerTest {
public void testChangeAggCluster() { public void testChangeAggCluster() {
InOrder inOrder = Mockito.inOrder(xdsConfigWatcher); InOrder inOrder = Mockito.inOrder(xdsConfigWatcher);
xdsDependencyManager = new XdsDependencyManager( xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
xdsClient, xdsConfigWatcher, syncContext, serverName, serverName); serverName, serverName, nameResolverArgs, scheduler);
inOrder.verify(xdsConfigWatcher, atLeastOnce()).onUpdate(any()); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(any());
// Setup initial config A -> A1 -> (A11, A12) // Setup initial config A -> A1 -> (A11, A12)
Cluster rootCluster = Cluster rootCluster =
@ -673,9 +673,8 @@ public class XdsDependencyManagerTest {
XdsTestUtils.addEdsClusters(clusterMap, edsMap, "clusterA11", "clusterA12"); XdsTestUtils.addEdsClusters(clusterMap, edsMap, "clusterA11", "clusterA12");
Listener clientListener = buildInlineClientListener(RDS_NAME, "root"); Listener clientListener = buildInlineClientListener(RDS_NAME, "root");
Listener serverListener = ControlPlaneRule.buildServerListener();
controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS,
ImmutableMap.of(XdsTestUtils.SERVER_LISTENER, serverListener, serverName, clientListener)); ImmutableMap.of(serverName, clientListener));
controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap); controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap);
controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap); controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap);
@ -702,96 +701,50 @@ public class XdsDependencyManagerTest {
inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(argThat(nameMatcher)); inOrder.verify(xdsConfigWatcher, timeout(1000)).onUpdate(argThat(nameMatcher));
} }
private Listener buildInlineClientListener(String rdsName, String clusterName) { @Test
return XdsTestUtils.buildInlineClientListener(rdsName, clusterName, serverName); public void testCdsError() throws IOException {
controlPlaneService.setXdsConfig(
ADS_TYPE_URL_CDS, ImmutableMap.of(XdsTestUtils.CLUSTER_NAME,
Cluster.newBuilder().setName(XdsTestUtils.CLUSTER_NAME).build()));
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
verify(xdsConfigWatcher, timeout(1000)).onUpdate(xdsUpdateCaptor.capture());
Status status = xdsUpdateCaptor.getValue().getValue()
.getClusters().get(CLUSTER_NAME).getStatus();
assertThat(status.getDescription()).contains(XdsTestUtils.CLUSTER_NAME);
} }
private Listener buildInlineClientListener(String rdsName, String clusterName) {
private static String toContextStr(String type, String resourceName) { return XdsTestUtils.buildInlineClientListener(rdsName, clusterName, serverName);
return type + " resource: " + resourceName;
} }
private static class TestWatcher implements XdsDependencyManager.XdsConfigWatcher { private static class TestWatcher implements XdsDependencyManager.XdsConfigWatcher {
XdsConfig lastConfig; XdsConfig lastConfig;
int numUpdates = 0; int numUpdates = 0;
int numError = 0; int numError = 0;
int numDoesNotExist = 0;
@Override @Override
public void onUpdate(XdsConfig config) { public void onUpdate(StatusOr<XdsConfig> update) {
log.fine("Config changed: " + config); log.fine("Config update: " + update);
lastConfig = config; if (update.hasValue()) {
numUpdates++; lastConfig = update.getValue();
} numUpdates++;
} else {
@Override numError++;
public void onError(String resourceContext, Status status) { }
log.fine(String.format("Error %s for %s: ", status, resourceContext));
numError++;
}
@Override
public void onResourceDoesNotExist(String resourceName) {
log.fine("Resource does not exist: " + resourceName);
numDoesNotExist++;
} }
private List<Integer> getStats() { private List<Integer> getStats() {
return Arrays.asList(numUpdates, numError, numDoesNotExist); return Arrays.asList(numUpdates, numError);
} }
private void verifyStats(int updt, int err, int notExist) { private void verifyStats(int updt, int err) {
assertThat(getStats()).isEqualTo(Arrays.asList(updt, err, notExist)); assertThat(getStats()).isEqualTo(Arrays.asList(updt, err));
} }
} }
private static class WrappedXdsClient extends XdsClient { static class ClusterNameMatcher implements ArgumentMatcher<StatusOr<XdsConfig>> {
private final XdsClient delegate;
private final SynchronizationContext syncContext;
private ResourceWatcher<LdsUpdate> ldsWatcher;
WrappedXdsClient(XdsClient delegate, SynchronizationContext syncContext) {
this.delegate = delegate;
this.syncContext = syncContext;
}
@Override
public void shutdown() {
delegate.shutdown();
}
@Override
@SuppressWarnings("unchecked")
public <T extends ResourceUpdate> void watchXdsResource(
XdsResourceType<T> type, String resourceName, ResourceWatcher<T> watcher,
Executor executor) {
if (type.equals(XdsListenerResource.getInstance())) {
ldsWatcher = (ResourceWatcher<LdsUpdate>) watcher;
}
delegate.watchXdsResource(type, resourceName, watcher, executor);
}
@Override
public <T extends ResourceUpdate> void cancelXdsResourceWatch(XdsResourceType<T> type,
String resourceName,
ResourceWatcher<T> watcher) {
delegate.cancelXdsResourceWatch(type, resourceName, watcher);
}
void deliverLdsUpdate(long httpMaxStreamDurationNano,
List<io.grpc.xds.VirtualHost> virtualHosts) {
syncContext.execute(() -> {
LdsUpdate ldsUpdate = LdsUpdate.forApiListener(
io.grpc.xds.HttpConnectionManager.forVirtualHosts(
httpMaxStreamDurationNano, virtualHosts, null));
ldsWatcher.onChanged(ldsUpdate);
});
}
}
static class ClusterNameMatcher implements ArgumentMatcher<XdsConfig> {
private final List<String> expectedNames; private final List<String> expectedNames;
ClusterNameMatcher(List<String> expectedNames) { ClusterNameMatcher(List<String> expectedNames) {
@ -799,7 +752,11 @@ public class XdsDependencyManagerTest {
} }
@Override @Override
public boolean matches(XdsConfig xdsConfig) { public boolean matches(StatusOr<XdsConfig> update) {
if (!update.hasValue()) {
return false;
}
XdsConfig xdsConfig = update.getValue();
if (xdsConfig == null || xdsConfig.getClusters() == null) { if (xdsConfig == null || xdsConfig.getClusters() == null) {
return false; return false;
} }

View File

@ -46,6 +46,7 @@ import com.google.protobuf.util.Durations;
import com.google.re2j.Pattern; import com.google.re2j.Pattern;
import io.grpc.CallOptions; import io.grpc.CallOptions;
import io.grpc.Channel; import io.grpc.Channel;
import io.grpc.ChannelLogger;
import io.grpc.ClientCall; import io.grpc.ClientCall;
import io.grpc.ClientInterceptor; import io.grpc.ClientInterceptor;
import io.grpc.ClientInterceptors; import io.grpc.ClientInterceptors;
@ -70,6 +71,7 @@ import io.grpc.Status.Code;
import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext;
import io.grpc.internal.AutoConfiguredLoadBalancerFactory; import io.grpc.internal.AutoConfiguredLoadBalancerFactory;
import io.grpc.internal.FakeClock; import io.grpc.internal.FakeClock;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.JsonParser; import io.grpc.internal.JsonParser;
import io.grpc.internal.JsonUtil; import io.grpc.internal.JsonUtil;
import io.grpc.internal.ObjectPool; import io.grpc.internal.ObjectPool;
@ -89,6 +91,8 @@ import io.grpc.xds.VirtualHost.Route.RouteAction.HashPolicy;
import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy; import io.grpc.xds.VirtualHost.Route.RouteAction.RetryPolicy;
import io.grpc.xds.VirtualHost.Route.RouteMatch; import io.grpc.xds.VirtualHost.Route.RouteMatch;
import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher; import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher;
import io.grpc.xds.XdsClusterResource.CdsUpdate;
import io.grpc.xds.XdsEndpointResource.EdsUpdate;
import io.grpc.xds.XdsListenerResource.LdsUpdate; import io.grpc.xds.XdsListenerResource.LdsUpdate;
import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate; import io.grpc.xds.XdsRouteConfigureResource.RdsUpdate;
import io.grpc.xds.client.Bootstrapper.AuthorityInfo; import io.grpc.xds.client.Bootstrapper.AuthorityInfo;
@ -104,6 +108,7 @@ import java.net.URISyntaxException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -187,6 +192,15 @@ public class XdsNameResolverTest {
private TestCall<?, ?> testCall; private TestCall<?, ?> testCall;
private boolean originalEnableTimeout; private boolean originalEnableTimeout;
private URI targetUri; private URI targetUri;
private final NameResolver.Args nameResolverArgs = NameResolver.Args.newBuilder()
.setDefaultPort(8080)
.setProxyDetector(GrpcUtil.DEFAULT_PROXY_DETECTOR)
.setSynchronizationContext(syncContext)
.setServiceConfigParser(mock(NameResolver.ServiceConfigParser.class))
.setChannelLogger(mock(ChannelLogger.class))
.setScheduledExecutorService(fakeClock.getScheduledExecutorService())
.build();
@Before @Before
public void setUp() { public void setUp() {
@ -213,7 +227,7 @@ public class XdsNameResolverTest {
resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null,
serviceConfigParser, syncContext, scheduler, serviceConfigParser, syncContext, scheduler,
xdsClientPoolFactory, mockRandom, filterRegistry, null, metricRecorder); xdsClientPoolFactory, mockRandom, filterRegistry, null, metricRecorder, nameResolverArgs);
} }
@After @After
@ -259,7 +273,7 @@ public class XdsNameResolverTest {
resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null,
serviceConfigParser, syncContext, scheduler, serviceConfigParser, syncContext, scheduler,
xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null,
metricRecorder); metricRecorder, nameResolverArgs);
resolver.start(mockListener); resolver.start(mockListener);
verify(mockListener).onError(errorCaptor.capture()); verify(mockListener).onError(errorCaptor.capture());
Status error = errorCaptor.getValue(); Status error = errorCaptor.getValue();
@ -273,7 +287,7 @@ public class XdsNameResolverTest {
resolver = new XdsNameResolver(targetUri, resolver = new XdsNameResolver(targetUri,
"notfound.google.com", AUTHORITY, null, serviceConfigParser, syncContext, scheduler, "notfound.google.com", AUTHORITY, null, serviceConfigParser, syncContext, scheduler,
xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null,
metricRecorder); metricRecorder, nameResolverArgs);
resolver.start(mockListener); resolver.start(mockListener);
verify(mockListener).onError(errorCaptor.capture()); verify(mockListener).onError(errorCaptor.capture());
Status error = errorCaptor.getValue(); Status error = errorCaptor.getValue();
@ -295,7 +309,7 @@ public class XdsNameResolverTest {
resolver = new XdsNameResolver( resolver = new XdsNameResolver(
targetUri, null, serviceAuthority, null, serviceConfigParser, syncContext, targetUri, null, serviceAuthority, null, serviceConfigParser, syncContext,
scheduler, xdsClientPoolFactory, scheduler, xdsClientPoolFactory,
mockRandom, FilterRegistry.getDefaultRegistry(), null, metricRecorder); mockRandom, FilterRegistry.getDefaultRegistry(), null, metricRecorder, nameResolverArgs);
resolver.start(mockListener); resolver.start(mockListener);
verify(mockListener, never()).onError(any(Status.class)); verify(mockListener, never()).onError(any(Status.class));
} }
@ -316,7 +330,7 @@ public class XdsNameResolverTest {
resolver = new XdsNameResolver( resolver = new XdsNameResolver(
targetUri, null, serviceAuthority, null, serviceConfigParser, syncContext, scheduler, targetUri, null, serviceAuthority, null, serviceConfigParser, syncContext, scheduler,
xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null,
metricRecorder); metricRecorder, nameResolverArgs);
resolver.start(mockListener); resolver.start(mockListener);
verify(mockListener, never()).onError(any(Status.class)); verify(mockListener, never()).onError(any(Status.class));
} }
@ -337,7 +351,7 @@ public class XdsNameResolverTest {
resolver = new XdsNameResolver( resolver = new XdsNameResolver(
targetUri, null, serviceAuthority, null, serviceConfigParser, syncContext, scheduler, targetUri, null, serviceAuthority, null, serviceConfigParser, syncContext, scheduler,
xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null,
metricRecorder); metricRecorder, nameResolverArgs);
// The Service Authority must be URL encoded, but unlike the LDS resource name. // The Service Authority must be URL encoded, but unlike the LDS resource name.
@ -366,7 +380,7 @@ public class XdsNameResolverTest {
resolver = new XdsNameResolver(targetUri, resolver = new XdsNameResolver(targetUri,
"xds.authority.com", serviceAuthority, null, serviceConfigParser, syncContext, scheduler, "xds.authority.com", serviceAuthority, null, serviceConfigParser, syncContext, scheduler,
xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null,
metricRecorder); metricRecorder, nameResolverArgs);
resolver.start(mockListener); resolver.start(mockListener);
verify(mockListener, never()).onError(any(Status.class)); verify(mockListener, never()).onError(any(Status.class));
} }
@ -399,7 +413,7 @@ public class XdsNameResolverTest {
resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null,
serviceConfigParser, syncContext, scheduler, serviceConfigParser, syncContext, scheduler,
xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null,
metricRecorder); metricRecorder, nameResolverArgs);
// use different ldsResourceName and service authority. The virtualhost lookup should use // use different ldsResourceName and service authority. The virtualhost lookup should use
// service authority. // service authority.
expectedLdsResourceName = "test-" + expectedLdsResourceName; expectedLdsResourceName = "test-" + expectedLdsResourceName;
@ -413,6 +427,7 @@ public class XdsNameResolverTest {
Collections.singletonList(route1), Collections.singletonList(route1),
ImmutableMap.of()); ImmutableMap.of());
xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost));
createAndDeliverClusterUpdates(xdsClient, cluster1);
verify(mockListener).onResult2(resolutionResultCaptor.capture()); verify(mockListener).onResult2(resolutionResultCaptor.capture());
assertServiceConfigForLoadBalancingConfig( assertServiceConfigForLoadBalancingConfig(
Collections.singletonList(cluster1), Collections.singletonList(cluster1),
@ -429,6 +444,7 @@ public class XdsNameResolverTest {
Collections.singletonList(route2), Collections.singletonList(route2),
ImmutableMap.of()); ImmutableMap.of());
xdsClient.deliverRdsUpdate(alternativeRdsResource, Collections.singletonList(virtualHost)); xdsClient.deliverRdsUpdate(alternativeRdsResource, Collections.singletonList(virtualHost));
createAndDeliverClusterUpdates(xdsClient, cluster2);
// Two new service config updates triggered: // Two new service config updates triggered:
// - with load balancing config being able to select cluster1 and cluster2 // - with load balancing config being able to select cluster1 and cluster2
// - with load balancing config being able to select cluster2 only // - with load balancing config being able to select cluster2 only
@ -467,6 +483,7 @@ public class XdsNameResolverTest {
Collections.singletonList(route), Collections.singletonList(route),
ImmutableMap.of()); ImmutableMap.of());
xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost));
createAndDeliverClusterUpdates(xdsClient, cluster1);
verify(mockListener).onResult2(resolutionResultCaptor.capture()); verify(mockListener).onResult2(resolutionResultCaptor.capture());
assertServiceConfigForLoadBalancingConfig( assertServiceConfigForLoadBalancingConfig(
Collections.singletonList(cluster1), Collections.singletonList(cluster1),
@ -483,6 +500,7 @@ public class XdsNameResolverTest {
verifyNoInteractions(mockListener); verifyNoInteractions(mockListener);
assertThat(xdsClient.rdsResource).isEqualTo(RDS_RESOURCE_NAME); assertThat(xdsClient.rdsResource).isEqualTo(RDS_RESOURCE_NAME);
xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost));
createAndDeliverClusterUpdates(xdsClient, cluster1);
verify(mockListener).onResult2(resolutionResultCaptor.capture()); verify(mockListener).onResult2(resolutionResultCaptor.capture());
assertServiceConfigForLoadBalancingConfig( assertServiceConfigForLoadBalancingConfig(
Collections.singletonList(cluster1), Collections.singletonList(cluster1),
@ -506,6 +524,7 @@ public class XdsNameResolverTest {
Collections.singletonList(route), Collections.singletonList(route),
ImmutableMap.of()); ImmutableMap.of());
xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost));
createAndDeliverClusterUpdates(xdsClient, cluster1);
verify(mockListener).onResult2(resolutionResultCaptor.capture()); verify(mockListener).onResult2(resolutionResultCaptor.capture());
assertServiceConfigForLoadBalancingConfig( assertServiceConfigForLoadBalancingConfig(
Collections.singletonList(cluster1), Collections.singletonList(cluster1),
@ -529,11 +548,15 @@ public class XdsNameResolverTest {
resolver.start(mockListener); resolver.start(mockListener);
FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient();
xdsClient.deliverError(Status.UNAVAILABLE.withDescription("server unreachable")); xdsClient.deliverError(Status.UNAVAILABLE.withDescription("server unreachable"));
verify(mockListener).onError(errorCaptor.capture()); verify(mockListener).onResult2(resolutionResultCaptor.capture());
Status error = errorCaptor.getValue(); InternalConfigSelector configSelector = resolutionResultCaptor.getValue()
.getAttributes().get(InternalConfigSelector.KEY);
Result selectResult = configSelector.selectConfig(
newPickSubchannelArgs(call1.methodDescriptor, new Metadata(), CallOptions.DEFAULT));
Status error = selectResult.getStatus();
assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE);
assertThat(error.getDescription()).isEqualTo("Unable to load LDS " + AUTHORITY assertThat(error.getDescription()).contains(AUTHORITY);
+ ". xDS server returned: UNAVAILABLE: server unreachable"); assertThat(error.getDescription()).contains("UNAVAILABLE: server unreachable");
} }
@Test @Test
@ -541,11 +564,15 @@ public class XdsNameResolverTest {
resolver.start(mockListener); resolver.start(mockListener);
FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient();
xdsClient.deliverError(Status.NOT_FOUND.withDescription("server unreachable")); xdsClient.deliverError(Status.NOT_FOUND.withDescription("server unreachable"));
verify(mockListener).onError(errorCaptor.capture()); verify(mockListener).onResult2(resolutionResultCaptor.capture());
Status error = errorCaptor.getValue(); InternalConfigSelector configSelector = resolutionResultCaptor.getValue()
.getAttributes().get(InternalConfigSelector.KEY);
Result selectResult = configSelector.selectConfig(
newPickSubchannelArgs(call1.methodDescriptor, new Metadata(), CallOptions.DEFAULT));
Status error = selectResult.getStatus();
assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE);
assertThat(error.getDescription()).isEqualTo("Unable to load LDS " + AUTHORITY assertThat(error.getDescription()).contains(AUTHORITY);
+ ". xDS server returned: NOT_FOUND: server unreachable"); assertThat(error.getDescription()).contains("NOT_FOUND: server unreachable");
assertThat(error.getCause()).isNull(); assertThat(error.getCause()).isNull();
} }
@ -555,15 +582,15 @@ public class XdsNameResolverTest {
FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient();
xdsClient.deliverLdsUpdateForRdsName(RDS_RESOURCE_NAME); xdsClient.deliverLdsUpdateForRdsName(RDS_RESOURCE_NAME);
xdsClient.deliverError(Status.UNAVAILABLE.withDescription("server unreachable")); xdsClient.deliverError(Status.UNAVAILABLE.withDescription("server unreachable"));
verify(mockListener, times(2)).onError(errorCaptor.capture()); verify(mockListener).onResult2(resolutionResultCaptor.capture());
Status error = errorCaptor.getAllValues().get(0); InternalConfigSelector configSelector = resolutionResultCaptor.getValue()
.getAttributes().get(InternalConfigSelector.KEY);
Result selectResult = configSelector.selectConfig(
newPickSubchannelArgs(call1.methodDescriptor, new Metadata(), CallOptions.DEFAULT));
Status error = selectResult.getStatus();
assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE); assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE);
assertThat(error.getDescription()).isEqualTo("Unable to load LDS " + AUTHORITY assertThat(error.getDescription()).contains(RDS_RESOURCE_NAME);
+ ". xDS server returned: UNAVAILABLE: server unreachable"); assertThat(error.getDescription()).contains("UNAVAILABLE: server unreachable");
error = errorCaptor.getAllValues().get(1);
assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE);
assertThat(error.getDescription()).isEqualTo("Unable to load RDS " + RDS_RESOURCE_NAME
+ ". xDS server returned: UNAVAILABLE: server unreachable");
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -581,10 +608,11 @@ public class XdsNameResolverTest {
resolver = new XdsNameResolver(targetUri, null, AUTHORITY, "random", resolver = new XdsNameResolver(targetUri, null, AUTHORITY, "random",
serviceConfigParser, syncContext, scheduler, serviceConfigParser, syncContext, scheduler,
xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null,
metricRecorder); metricRecorder, nameResolverArgs);
resolver.start(mockListener); resolver.start(mockListener);
FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient();
xdsClient.deliverLdsUpdate(0L, Arrays.asList(virtualHost)); xdsClient.deliverLdsUpdate(0L, Arrays.asList(virtualHost));
createAndDeliverClusterUpdates(xdsClient, cluster1);
verify(mockListener).onResult2(resolutionResultCaptor.capture()); verify(mockListener).onResult2(resolutionResultCaptor.capture());
assertServiceConfigForLoadBalancingConfig( assertServiceConfigForLoadBalancingConfig(
Collections.singletonList(cluster1), Collections.singletonList(cluster1),
@ -605,10 +633,11 @@ public class XdsNameResolverTest {
resolver = new XdsNameResolver(targetUri, null, AUTHORITY, "random", resolver = new XdsNameResolver(targetUri, null, AUTHORITY, "random",
serviceConfigParser, syncContext, scheduler, serviceConfigParser, syncContext, scheduler,
xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null,
metricRecorder); metricRecorder, nameResolverArgs);
resolver.start(mockListener); resolver.start(mockListener);
FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient();
xdsClient.deliverLdsUpdate(0L, Arrays.asList(virtualHost)); xdsClient.deliverLdsUpdateOnly(0L, Arrays.asList(virtualHost));
fakeClock.forwardTime(15, TimeUnit.SECONDS);
assertEmptyResolutionResult("random"); assertEmptyResolutionResult("random");
} }
@ -617,7 +646,7 @@ public class XdsNameResolverTest {
resolver = new XdsNameResolver(targetUri, null, AUTHORITY, AUTHORITY, resolver = new XdsNameResolver(targetUri, null, AUTHORITY, AUTHORITY,
serviceConfigParser, syncContext, scheduler, serviceConfigParser, syncContext, scheduler,
xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null,
metricRecorder); metricRecorder, nameResolverArgs);
resolver.start(mockListener); resolver.start(mockListener);
FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient();
xdsClient.deliverLdsUpdate(0L, buildUnmatchedVirtualHosts()); xdsClient.deliverLdsUpdate(0L, buildUnmatchedVirtualHosts());
@ -702,7 +731,7 @@ public class XdsNameResolverTest {
true, 5, 5, new AutoConfiguredLoadBalancerFactory("pick-first")); true, 5, 5, new AutoConfiguredLoadBalancerFactory("pick-first"));
resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, realParser, syncContext, resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, realParser, syncContext,
scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null,
metricRecorder); metricRecorder, nameResolverArgs);
resolver.start(mockListener); resolver.start(mockListener);
FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient();
RetryPolicy retryPolicy = RetryPolicy.create( RetryPolicy retryPolicy = RetryPolicy.create(
@ -913,7 +942,7 @@ public class XdsNameResolverTest {
resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser,
syncContext, scheduler, syncContext, scheduler,
xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null,
metricRecorder); metricRecorder, nameResolverArgs);
resolver.start(mockListener); resolver.start(mockListener);
xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient = (FakeXdsClient) resolver.getXdsClient();
xdsClient.deliverLdsUpdate( xdsClient.deliverLdsUpdate(
@ -946,7 +975,7 @@ public class XdsNameResolverTest {
public void resolved_routeActionHasAutoHostRewrite_emitsCallOptionForTheSame() { public void resolved_routeActionHasAutoHostRewrite_emitsCallOptionForTheSame() {
resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser,
syncContext, scheduler, xdsClientPoolFactory, mockRandom, syncContext, scheduler, xdsClientPoolFactory, mockRandom,
FilterRegistry.getDefaultRegistry(), null, metricRecorder); FilterRegistry.getDefaultRegistry(), null, metricRecorder, nameResolverArgs);
resolver.start(mockListener); resolver.start(mockListener);
FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient();
xdsClient.deliverLdsUpdate( xdsClient.deliverLdsUpdate(
@ -977,7 +1006,7 @@ public class XdsNameResolverTest {
public void resolved_routeActionNoAutoHostRewrite_doesntEmitCallOptionForTheSame() { public void resolved_routeActionNoAutoHostRewrite_doesntEmitCallOptionForTheSame() {
resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser,
syncContext, scheduler, xdsClientPoolFactory, mockRandom, syncContext, scheduler, xdsClientPoolFactory, mockRandom,
FilterRegistry.getDefaultRegistry(), null, metricRecorder); FilterRegistry.getDefaultRegistry(), null, metricRecorder, nameResolverArgs);
resolver.start(mockListener); resolver.start(mockListener);
FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient();
xdsClient.deliverLdsUpdate( xdsClient.deliverLdsUpdate(
@ -1190,6 +1219,20 @@ public class XdsNameResolverTest {
assertCallSelectClusterResult(call1, configSelector, cluster1, 20.0); assertCallSelectClusterResult(call1, configSelector, cluster1, 20.0);
} }
/** Creates and delivers both CDS and EDS updates for the given clusters. */
private static void createAndDeliverClusterUpdates(
FakeXdsClient xdsClient, String... clusterNames) {
for (String clusterName : clusterNames) {
CdsUpdate.Builder forEds = CdsUpdate
.forEds(clusterName, clusterName, null, null, null, null, false)
.roundRobinLbPolicy();
xdsClient.deliverCdsUpdate(clusterName, forEds.build());
EdsUpdate edsUpdate = new EdsUpdate(clusterName,
XdsTestUtils.createMinimalLbEndpointsMap("host"), Collections.emptyList());
xdsClient.deliverEdsUpdate(clusterName, edsUpdate);
}
}
@Test @Test
public void resolved_simpleCallSucceeds_routeToRls() { public void resolved_simpleCallSucceeds_routeToRls() {
when(mockRandom.nextInt(anyInt())).thenReturn(90, 10); when(mockRandom.nextInt(anyInt())).thenReturn(90, 10);
@ -1305,6 +1348,7 @@ public class XdsNameResolverTest {
// LDS 1. // LDS 1.
xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2));
createAndDeliverClusterUpdates(xdsClient, cluster1);
assertClusterResolutionResult(call1, cluster1); assertClusterResolutionResult(call1, cluster1);
ImmutableList<StatefulFilter> lds1Snapshot = statefulFilterProvider.getAllInstances(); ImmutableList<StatefulFilter> lds1Snapshot = statefulFilterProvider.getAllInstances();
// Verify that StatefulFilter with different filter names result in different Filter instances. // Verify that StatefulFilter with different filter names result in different Filter instances.
@ -1359,7 +1403,7 @@ public class XdsNameResolverTest {
* Verifies the lifecycle of HCM filter instances across RDS updates. * Verifies the lifecycle of HCM filter instances across RDS updates.
* *
* <p>Filter instances: * <p>Filter instances:
* 1. Must have instantiated by the initial LDS. * 1. Must have instantiated by the initial LDS/RDS.
* 2. Must be reused by all subsequent RDS updates. * 2. Must be reused by all subsequent RDS updates.
* 3. Must be not shutdown (closed) by valid RDS updates. * 3. Must be not shutdown (closed) by valid RDS updates.
*/ */
@ -1371,22 +1415,19 @@ public class XdsNameResolverTest {
// LDS 1. // LDS 1.
xdsClient.deliverLdsUpdateForRdsNameWithFilters(RDS_RESOURCE_NAME, xdsClient.deliverLdsUpdateForRdsNameWithFilters(RDS_RESOURCE_NAME,
filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); filterStateTestConfigs(STATEFUL_1, STATEFUL_2));
ImmutableList<StatefulFilter> lds1Snapshot = statefulFilterProvider.getAllInstances();
// Verify that StatefulFilter with different filter names result in different Filter instances.
assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2);
// Naming: lds<LDS#>Filter<name#>
StatefulFilter lds1Filter1 = lds1Snapshot.get(0);
StatefulFilter lds1Filter2 = lds1Snapshot.get(1);
assertThat(lds1Filter1).isNotSameInstanceAs(lds1Filter2);
// RDS 1. // RDS 1.
VirtualHost vhost1 = filterStateTestVhost(); VirtualHost vhost1 = filterStateTestVhost();
xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, vhost1); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, vhost1);
createAndDeliverClusterUpdates(xdsClient, cluster1);
assertClusterResolutionResult(call1, cluster1); assertClusterResolutionResult(call1, cluster1);
// Initial RDS update should not generate Filter instances. // Initial RDS update should not generate Filter instances.
ImmutableList<StatefulFilter> rds1Snapshot = statefulFilterProvider.getAllInstances(); ImmutableList<StatefulFilter> rds1Snapshot = statefulFilterProvider.getAllInstances();
assertWithMessage("RDS 1: Expected Filter instances to be reused across RDS route updates") // Verify that StatefulFilter with different filter names result in different Filter instances.
.that(rds1Snapshot).isEqualTo(lds1Snapshot); assertWithMessage("RDS 1: expected to create filter instances").that(rds1Snapshot).hasSize(2);
// Naming: lds<LDS#>Filter<name#>
StatefulFilter lds1Filter1 = rds1Snapshot.get(0);
StatefulFilter lds1Filter2 = rds1Snapshot.get(1);
assertThat(lds1Filter1).isNotSameInstanceAs(lds1Filter2);
// RDS 2: exactly the same as RDS 1. // RDS 2: exactly the same as RDS 1.
xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, vhost1); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, vhost1);
@ -1394,7 +1435,7 @@ public class XdsNameResolverTest {
ImmutableList<StatefulFilter> rds2Snapshot = statefulFilterProvider.getAllInstances(); ImmutableList<StatefulFilter> rds2Snapshot = statefulFilterProvider.getAllInstances();
// Neither should any subsequent RDS updates. // Neither should any subsequent RDS updates.
assertWithMessage("RDS 2: Expected Filter instances to be reused across RDS route updates") assertWithMessage("RDS 2: Expected Filter instances to be reused across RDS route updates")
.that(rds2Snapshot).isEqualTo(lds1Snapshot); .that(rds2Snapshot).isEqualTo(rds1Snapshot);
// RDS 3: Contains a per-route override for STATEFUL_1. // RDS 3: Contains a per-route override for STATEFUL_1.
VirtualHost vhost3 = filterStateTestVhost(ImmutableMap.of( VirtualHost vhost3 = filterStateTestVhost(ImmutableMap.of(
@ -1406,7 +1447,7 @@ public class XdsNameResolverTest {
// As with any other Route update, typed_per_filter_config overrides should not result in // As with any other Route update, typed_per_filter_config overrides should not result in
// creating new filter instances. // creating new filter instances.
assertWithMessage("RDS 3: Expected Filter instances to be reused on per-route filter overrides") assertWithMessage("RDS 3: Expected Filter instances to be reused on per-route filter overrides")
.that(rds3Snapshot).isEqualTo(lds1Snapshot); .that(rds3Snapshot).isEqualTo(rds1Snapshot);
} }
/** /**
@ -1427,7 +1468,7 @@ public class XdsNameResolverTest {
.register(statefulFilterProvider, altStatefulFilterProvider, ROUTER_FILTER_PROVIDER); .register(statefulFilterProvider, altStatefulFilterProvider, ROUTER_FILTER_PROVIDER);
resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser,
syncContext, scheduler, xdsClientPoolFactory, mockRandom, filterRegistry, null, syncContext, scheduler, xdsClientPoolFactory, mockRandom, filterRegistry, null,
metricRecorder); metricRecorder, nameResolverArgs);
resolver.start(mockListener); resolver.start(mockListener);
FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient();
@ -1435,6 +1476,7 @@ public class XdsNameResolverTest {
// LDS 1. // LDS 1.
xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2));
createAndDeliverClusterUpdates(xdsClient, cluster1);
assertClusterResolutionResult(call1, cluster1); assertClusterResolutionResult(call1, cluster1);
ImmutableList<StatefulFilter> lds1Snapshot = statefulFilterProvider.getAllInstances(); ImmutableList<StatefulFilter> lds1Snapshot = statefulFilterProvider.getAllInstances();
ImmutableList<StatefulFilter> lds1SnapshotAlt = altStatefulFilterProvider.getAllInstances(); ImmutableList<StatefulFilter> lds1SnapshotAlt = altStatefulFilterProvider.getAllInstances();
@ -1483,6 +1525,7 @@ public class XdsNameResolverTest {
// LDS 1. // LDS 1.
xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2));
createAndDeliverClusterUpdates(xdsClient, cluster1);
assertClusterResolutionResult(call1, cluster1); assertClusterResolutionResult(call1, cluster1);
ImmutableList<StatefulFilter> lds1Snapshot = statefulFilterProvider.getAllInstances(); ImmutableList<StatefulFilter> lds1Snapshot = statefulFilterProvider.getAllInstances();
assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2);
@ -1510,6 +1553,7 @@ public class XdsNameResolverTest {
// LDS 1. // LDS 1.
xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); xdsClient.deliverLdsUpdateWithFilters(vhost, filterStateTestConfigs(STATEFUL_1, STATEFUL_2));
createAndDeliverClusterUpdates(xdsClient, cluster1);
assertClusterResolutionResult(call1, cluster1); assertClusterResolutionResult(call1, cluster1);
ImmutableList<StatefulFilter> lds1Snapshot = statefulFilterProvider.getAllInstances(); ImmutableList<StatefulFilter> lds1Snapshot = statefulFilterProvider.getAllInstances();
assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2); assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2);
@ -1526,33 +1570,32 @@ public class XdsNameResolverTest {
} }
/** /**
* Verifies that filter instances are NOT shutdown on RDS_RESOURCE_NAME not found. * Verifies that all filter instances are shutdown (closed) on RDS resource not found.
*/ */
@Test @Test
public void filterState_shutdown_noShutdownOnRdsNotFound() { public void filterState_shutdown_onRdsNotFound() {
StatefulFilter.Provider statefulFilterProvider = filterStateTestSetupResolver(); StatefulFilter.Provider statefulFilterProvider = filterStateTestSetupResolver();
FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient();
// LDS 1. // LDS 1.
xdsClient.deliverLdsUpdateForRdsNameWithFilters(RDS_RESOURCE_NAME, xdsClient.deliverLdsUpdateForRdsNameWithFilters(RDS_RESOURCE_NAME,
filterStateTestConfigs(STATEFUL_1, STATEFUL_2)); filterStateTestConfigs(STATEFUL_1, STATEFUL_2));
ImmutableList<StatefulFilter> lds1Snapshot = statefulFilterProvider.getAllInstances();
assertWithMessage("LDS 1: expected to create filter instances").that(lds1Snapshot).hasSize(2);
// Naming: lds<LDS#>Filter<name#>
StatefulFilter lds1Filter1 = lds1Snapshot.get(0);
StatefulFilter lds1Filter2 = lds1Snapshot.get(1);
// RDS 1: Standard vhost with a route. // RDS 1: Standard vhost with a route.
xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, filterStateTestVhost()); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, filterStateTestVhost());
createAndDeliverClusterUpdates(xdsClient, cluster1);
assertClusterResolutionResult(call1, cluster1); assertClusterResolutionResult(call1, cluster1);
assertThat(statefulFilterProvider.getAllInstances()).isEqualTo(lds1Snapshot); ImmutableList<StatefulFilter> rds1Snapshot = statefulFilterProvider.getAllInstances();
assertWithMessage("RDS 1: expected to create filter instances").that(rds1Snapshot).hasSize(2);
// Naming: lds<LDS#>Filter<name#>
StatefulFilter lds1Filter1 = rds1Snapshot.get(0);
StatefulFilter lds1Filter2 = rds1Snapshot.get(1);
// RDS 2: RDS_RESOURCE_NAME not found. // RDS 2: RDS_RESOURCE_NAME not found.
reset(mockListener); reset(mockListener);
xdsClient.deliverRdsResourceNotFound(RDS_RESOURCE_NAME); xdsClient.deliverRdsResourceNotFound(RDS_RESOURCE_NAME);
assertEmptyResolutionResult(RDS_RESOURCE_NAME); assertEmptyResolutionResult(RDS_RESOURCE_NAME);
assertThat(lds1Filter1.isShutdown()).isFalse(); assertThat(lds1Filter1.isShutdown()).isTrue();
assertThat(lds1Filter2.isShutdown()).isFalse(); assertThat(lds1Filter2.isShutdown()).isTrue();
} }
private StatefulFilter.Provider filterStateTestSetupResolver() { private StatefulFilter.Provider filterStateTestSetupResolver() {
@ -1561,7 +1604,7 @@ public class XdsNameResolverTest {
.register(statefulFilterProvider, ROUTER_FILTER_PROVIDER); .register(statefulFilterProvider, ROUTER_FILTER_PROVIDER);
resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser, resolver = new XdsNameResolver(targetUri, null, AUTHORITY, null, serviceConfigParser,
syncContext, scheduler, xdsClientPoolFactory, mockRandom, filterRegistry, null, syncContext, scheduler, xdsClientPoolFactory, mockRandom, filterRegistry, null,
metricRecorder); metricRecorder, nameResolverArgs);
resolver.start(mockListener); resolver.start(mockListener);
return statefulFilterProvider; return statefulFilterProvider;
} }
@ -1762,6 +1805,7 @@ public class XdsNameResolverTest {
ImmutableList.of(route1, route2, route3), ImmutableList.of(route1, route2, route3),
ImmutableMap.of()); ImmutableMap.of());
xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost));
createAndDeliverClusterUpdates(xdsClient, "cluster-foo", "cluster-bar", "cluster-baz");
verify(mockListener).onResult2(resolutionResultCaptor.capture()); verify(mockListener).onResult2(resolutionResultCaptor.capture());
String expectedServiceConfigJson = String expectedServiceConfigJson =
@ -2385,6 +2429,8 @@ public class XdsNameResolverTest {
private String rdsResource; private String rdsResource;
private ResourceWatcher<LdsUpdate> ldsWatcher; private ResourceWatcher<LdsUpdate> ldsWatcher;
private ResourceWatcher<RdsUpdate> rdsWatcher; private ResourceWatcher<RdsUpdate> rdsWatcher;
private final Map<String, List<ResourceWatcher<CdsUpdate>>> cdsWatchers = new HashMap<>();
private final Map<String, List<ResourceWatcher<EdsUpdate>>> edsWatchers = new HashMap<>();
@Override @Override
public BootstrapInfo getBootstrapInfo() { public BootstrapInfo getBootstrapInfo() {
@ -2412,10 +2458,19 @@ public class XdsNameResolverTest {
rdsResource = resourceName; rdsResource = resourceName;
rdsWatcher = (ResourceWatcher<RdsUpdate>) watcher; rdsWatcher = (ResourceWatcher<RdsUpdate>) watcher;
break; break;
case "CDS":
cdsWatchers.computeIfAbsent(resourceName, k -> new ArrayList<>())
.add((ResourceWatcher<CdsUpdate>) watcher);
break;
case "EDS":
edsWatchers.computeIfAbsent(resourceName, k -> new ArrayList<>())
.add((ResourceWatcher<EdsUpdate>) watcher);
break;
default: default:
} }
} }
@SuppressWarnings("unchecked")
@Override @Override
public <T extends ResourceUpdate> void cancelXdsResourceWatch(XdsResourceType<T> type, public <T extends ResourceUpdate> void cancelXdsResourceWatch(XdsResourceType<T> type,
String resourceName, String resourceName,
@ -2434,25 +2489,53 @@ public class XdsNameResolverTest {
rdsResource = null; rdsResource = null;
rdsWatcher = null; rdsWatcher = null;
break; break;
case "CDS":
assertThat(cdsWatchers).containsKey(resourceName);
assertThat(cdsWatchers.get(resourceName)).contains(watcher);
cdsWatchers.get(resourceName).remove((ResourceWatcher<CdsUpdate>) watcher);
break;
case "EDS":
assertThat(edsWatchers).containsKey(resourceName);
assertThat(edsWatchers.get(resourceName)).contains(watcher);
edsWatchers.get(resourceName).remove((ResourceWatcher<EdsUpdate>) watcher);
break;
default: default:
} }
} }
void deliverLdsUpdate(long httpMaxStreamDurationNano, List<VirtualHost> virtualHosts) { void deliverLdsUpdateOnly(long httpMaxStreamDurationNano, List<VirtualHost> virtualHosts) {
syncContext.execute(() -> { syncContext.execute(() -> {
ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts(
httpMaxStreamDurationNano, virtualHosts, null))); httpMaxStreamDurationNano, virtualHosts, null)));
}); });
} }
void deliverLdsUpdate(long httpMaxStreamDurationNano, List<VirtualHost> virtualHosts) {
List<String> clusterNames = new ArrayList<>();
for (VirtualHost vh : virtualHosts) {
clusterNames.addAll(getClusterNames(vh.routes()));
}
syncContext.execute(() -> {
ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts(
httpMaxStreamDurationNano, virtualHosts, null)));
createAndDeliverClusterUpdates(this, clusterNames.toArray(new String[0]));
});
}
void deliverLdsUpdate(final List<Route> routes) { void deliverLdsUpdate(final List<Route> routes) {
VirtualHost virtualHost = VirtualHost virtualHost =
VirtualHost.create( VirtualHost.create(
"virtual-host", Collections.singletonList(expectedLdsResourceName), routes, "virtual-host", Collections.singletonList(expectedLdsResourceName), routes,
ImmutableMap.of()); ImmutableMap.of());
List<String> clusterNames = getClusterNames(routes);
syncContext.execute(() -> { syncContext.execute(() -> {
ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts(
0L, Collections.singletonList(virtualHost), null))); 0L, Collections.singletonList(virtualHost), null)));
if (!clusterNames.isEmpty()) {
createAndDeliverClusterUpdates(this, clusterNames.toArray(new String[0]));
}
}); });
} }
@ -2508,6 +2591,7 @@ public class XdsNameResolverTest {
syncContext.execute(() -> { syncContext.execute(() -> {
ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts( ldsWatcher.onChanged(LdsUpdate.forApiListener(HttpConnectionManager.forVirtualHosts(
0L, Collections.singletonList(virtualHost), filterChain))); 0L, Collections.singletonList(virtualHost), filterChain)));
createAndDeliverClusterUpdates(this, cluster);
}); });
} }
@ -2545,6 +2629,29 @@ public class XdsNameResolverTest {
}); });
} }
private List<String> getClusterNames(List<Route> routes) {
List<String> clusterNames = new ArrayList<>();
for (Route r : routes) {
if (r.routeAction() == null) {
continue;
}
String cluster = r.routeAction().cluster();
if (cluster != null) {
clusterNames.add(cluster);
} else {
List<ClusterWeight> weightedClusters = r.routeAction().weightedClusters();
if (weightedClusters == null) {
continue;
}
for (ClusterWeight wc : weightedClusters) {
clusterNames.add(wc.name());
}
}
}
return clusterNames;
}
void deliverRdsUpdateWithFaultInjection( void deliverRdsUpdateWithFaultInjection(
String resourceName, @Nullable FaultConfig virtualHostFaultConfig, String resourceName, @Nullable FaultConfig virtualHostFaultConfig,
@Nullable FaultConfig routFaultConfig, @Nullable FaultConfig weightedClusterFaultConfig) { @Nullable FaultConfig routFaultConfig, @Nullable FaultConfig weightedClusterFaultConfig) {
@ -2581,6 +2688,7 @@ public class XdsNameResolverTest {
overrideConfig); overrideConfig);
syncContext.execute(() -> { syncContext.execute(() -> {
rdsWatcher.onChanged(new RdsUpdate(Collections.singletonList(virtualHost))); rdsWatcher.onChanged(new RdsUpdate(Collections.singletonList(virtualHost)));
createAndDeliverClusterUpdates(this, cluster1);
}); });
} }
@ -2606,6 +2714,29 @@ public class XdsNameResolverTest {
}); });
} }
private void deliverCdsUpdate(String clusterName, CdsUpdate update) {
if (!cdsWatchers.containsKey(clusterName)) {
return;
}
syncContext.execute(() -> {
List<ResourceWatcher<CdsUpdate>> resourceWatchers =
ImmutableList.copyOf(cdsWatchers.get(clusterName));
resourceWatchers.forEach(w -> w.onChanged(update));
});
}
private void deliverEdsUpdate(String name, EdsUpdate update) {
syncContext.execute(() -> {
if (!edsWatchers.containsKey(name)) {
return;
}
List<ResourceWatcher<EdsUpdate>> resourceWatchers =
ImmutableList.copyOf(edsWatchers.get(name));
resourceWatchers.forEach(w -> w.onChanged(update));
});
}
void deliverError(final Status error) { void deliverError(final Status error) {
if (ldsWatcher != null) { if (ldsWatcher != null) {
syncContext.execute(() -> { syncContext.execute(() -> {
@ -2617,6 +2748,11 @@ public class XdsNameResolverTest {
rdsWatcher.onError(error); rdsWatcher.onError(error);
}); });
} }
syncContext.execute(() -> {
cdsWatchers.values().stream()
.flatMap(List::stream)
.forEach(w -> w.onError(error));
});
} }
} }

View File

@ -51,7 +51,6 @@ import io.envoyproxy.envoy.service.load_stats.v3.LoadStatsResponse;
import io.grpc.BindableService; import io.grpc.BindableService;
import io.grpc.Context; import io.grpc.Context;
import io.grpc.Context.CancellationListener; import io.grpc.Context.CancellationListener;
import io.grpc.Status;
import io.grpc.StatusOr; import io.grpc.StatusOr;
import io.grpc.internal.JsonParser; import io.grpc.internal.JsonParser;
import io.grpc.stub.StreamObserver; import io.grpc.stub.StreamObserver;
@ -281,6 +280,16 @@ public class XdsTestUtils {
return builder.build(); return builder.build();
} }
static Map<Locality, LocalityLbEndpoints> createMinimalLbEndpointsMap(String serverHostName) {
Map<Locality, LocalityLbEndpoints> lbEndpointsMap = new HashMap<>();
LbEndpoint lbEndpoint = LbEndpoint.create(
serverHostName, ENDPOINT_PORT, 0, true, ENDPOINT_HOSTNAME, ImmutableMap.of());
lbEndpointsMap.put(
Locality.create("", "", ""),
LocalityLbEndpoints.create(ImmutableList.of(lbEndpoint), 10, 0, ImmutableMap.of()));
return lbEndpointsMap;
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private static ImmutableMap<String, ?> getWrrLbConfigAsMap() throws IOException { private static ImmutableMap<String, ?> getWrrLbConfigAsMap() throws IOException {
String lbConfigStr = "{\"wrr_locality_experimental\" : " String lbConfigStr = "{\"wrr_locality_experimental\" : "
@ -353,7 +362,6 @@ public class XdsTestUtils {
return Listener.newBuilder() return Listener.newBuilder()
.setName(serverName) .setName(serverName)
.setApiListener(clientListenerBuilder.build()).build(); .setApiListener(clientListenerBuilder.build()).build();
} }
/** /**
@ -407,18 +415,4 @@ public class XdsTestUtils {
responseObserver.onNext(response); responseObserver.onNext(response);
} }
} }
static class StatusMatcher implements ArgumentMatcher<Status> {
private final Status expectedStatus;
StatusMatcher(Status expectedStatus) {
this.expectedStatus = expectedStatus;
}
@Override
public boolean matches(Status status) {
return status != null && expectedStatus.getCode().equals(status.getCode())
&& expectedStatus.getDescription().equals(status.getDescription());
}
}
} }