From 18e7e2ddca1323bcdfec4649d7a1dd91a68f8223 Mon Sep 17 00:00:00 2001 From: Chengyuan Zhang Date: Thu, 8 Oct 2020 00:57:26 -0700 Subject: [PATCH] xds: promote XdsClientImpl2 (#7484) Replace the old XdsClient implementation with the new one that supports watching multiple LDS/RDS resources separately. --- .../main/java/io/grpc/xds/XdsClientImpl.java | 1954 --------- .../main/java/io/grpc/xds/XdsClientImpl2.java | 3 +- .../xds/XdsClientWrapperForServerSds.java | 6 +- .../java/io/grpc/xds/XdsClientImplTest.java | 3739 ----------------- .../xds/XdsClientImplTestForListener.java | 104 +- 5 files changed, 42 insertions(+), 5764 deletions(-) delete mode 100644 xds/src/main/java/io/grpc/xds/XdsClientImpl.java delete mode 100644 xds/src/test/java/io/grpc/xds/XdsClientImplTest.java diff --git a/xds/src/main/java/io/grpc/xds/XdsClientImpl.java b/xds/src/main/java/io/grpc/xds/XdsClientImpl.java deleted file mode 100644 index 507aeca322..0000000000 --- a/xds/src/main/java/io/grpc/xds/XdsClientImpl.java +++ /dev/null @@ -1,1954 +0,0 @@ -/* - * Copyright 2019 The gRPC Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.grpc.xds; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Stopwatch; -import com.google.common.base.Supplier; -import com.google.common.collect.ImmutableList; -import com.google.protobuf.Any; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.MessageOrBuilder; -import com.google.protobuf.util.JsonFormat; -import com.google.rpc.Code; -import io.envoyproxy.envoy.config.cluster.v3.Cluster; -import io.envoyproxy.envoy.config.cluster.v3.Cluster.DiscoveryType; -import io.envoyproxy.envoy.config.cluster.v3.Cluster.EdsClusterConfig; -import io.envoyproxy.envoy.config.cluster.v3.Cluster.LbPolicy; -import io.envoyproxy.envoy.config.core.v3.Address; -import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; -import io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint; -import io.envoyproxy.envoy.config.listener.v3.FilterChain; -import io.envoyproxy.envoy.config.listener.v3.FilterChainMatch; -import io.envoyproxy.envoy.config.listener.v3.Listener; -import io.envoyproxy.envoy.config.route.v3.Route; -import io.envoyproxy.envoy.config.route.v3.RouteConfiguration; -import io.envoyproxy.envoy.config.route.v3.VirtualHost; -import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager; -import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.Rds; -import io.envoyproxy.envoy.service.discovery.v3.AggregatedDiscoveryServiceGrpc; -import io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest; -import io.envoyproxy.envoy.service.discovery.v3.DiscoveryResponse; -import io.grpc.InternalLogId; -import io.grpc.Status; -import io.grpc.SynchronizationContext; -import io.grpc.SynchronizationContext.ScheduledHandle; -import io.grpc.internal.BackoffPolicy; -import io.grpc.stub.StreamObserver; -import io.grpc.xds.EnvoyProtoData.DropOverload; -import io.grpc.xds.EnvoyProtoData.Locality; -import io.grpc.xds.EnvoyProtoData.LocalityLbEndpoints; -import io.grpc.xds.EnvoyProtoData.Node; -import io.grpc.xds.EnvoyProtoData.StructOrError; -import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; -import io.grpc.xds.LoadStatsManager.LoadStatsStore; -import io.grpc.xds.XdsLogger.XdsLogLevel; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; - -final class XdsClientImpl extends XdsClient { - - // Longest time to wait, since the subscription to some resource, for concluding its absence. - @VisibleForTesting - static final int INITIAL_RESOURCE_FETCH_TIMEOUT_SEC = 15; - - @VisibleForTesting - static final String ADS_TYPE_URL_LDS_V2 = "type.googleapis.com/envoy.api.v2.Listener"; - @VisibleForTesting - static final String ADS_TYPE_URL_LDS = - "type.googleapis.com/envoy.config.listener.v3.Listener"; - @VisibleForTesting - static final String ADS_TYPE_URL_RDS_V2 = - "type.googleapis.com/envoy.api.v2.RouteConfiguration"; - @VisibleForTesting - static final String ADS_TYPE_URL_RDS = - "type.googleapis.com/envoy.config.route.v3.RouteConfiguration"; - private static final String TYPE_URL_HTTP_CONNECTION_MANAGER_V2 = - "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2" - + ".HttpConnectionManager"; - private static final String TYPE_URL_HTTP_CONNECTION_MANAGER = - "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3" - + ".HttpConnectionManager"; - @VisibleForTesting - static final String ADS_TYPE_URL_CDS_V2 = "type.googleapis.com/envoy.api.v2.Cluster"; - @VisibleForTesting - static final String ADS_TYPE_URL_CDS = - "type.googleapis.com/envoy.config.cluster.v3.Cluster"; - @VisibleForTesting - static final String ADS_TYPE_URL_EDS_V2 = - "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment"; - @VisibleForTesting - static final String ADS_TYPE_URL_EDS = - "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment"; - - private final MessagePrinter respPrinter = new MessagePrinter(); - - private final InternalLogId logId; - private final XdsLogger logger; - // Name of the target server this gRPC client is trying to talk to. - private final String targetName; - private final XdsChannel xdsChannel; - private final SynchronizationContext syncContext; - private final ScheduledExecutorService timeService; - private final BackoffPolicy.Provider backoffPolicyProvider; - private final Supplier stopwatchSupplier; - private final Stopwatch adsStreamRetryStopwatch; - // The node identifier to be included in xDS requests. Management server only requires the - // first request to carry the node identifier on a stream. It should be identical if present - // more than once. - private Node node; - - private final Map cdsResourceSubscribers = new HashMap<>(); - private final Map edsResourceSubscribers = new HashMap<>(); - - private final LoadStatsManager loadStatsManager = new LoadStatsManager(); - - // Last successfully applied version_info for each resource type. Starts with empty string. - // A version_info is used to update management server with client's most recent knowledge of - // resources. - private String ldsVersion = ""; - private String rdsVersion = ""; - private String cdsVersion = ""; - private String edsVersion = ""; - - // Timer for concluding the currently requesting LDS resource not found. - @Nullable - private ScheduledHandle ldsRespTimer; - - // Timer for concluding the currently requesting RDS resource not found. - @Nullable - private ScheduledHandle rdsRespTimer; - - @Nullable - private AbstractAdsStream adsStream; - @Nullable - private BackoffPolicy retryBackoffPolicy; - @Nullable - private ScheduledHandle rpcRetryTimer; - @Nullable - private LoadReportClient lrsClient; - private int loadReportCount; // number of clusters enabling load reporting - - // Following fields are set only after the ConfigWatcher registered. Once set, they should - // never change. Only a ConfigWatcher or ListenerWatcher can be registered. - @Nullable - private ConfigWatcher configWatcher; - // The "xds:" URI (including port suffix if present) that the gRPC client targets for. - @Nullable - private String ldsResourceName; - - // only a ConfigWatcher or ListenerWatcher can be registered. - @Nullable - private ListenerWatcher listenerWatcher; - private int listenerPort = -1; - - XdsClientImpl( - String targetName, - XdsChannel channel, - Node node, - SynchronizationContext syncContext, - ScheduledExecutorService timeService, - BackoffPolicy.Provider backoffPolicyProvider, - Supplier stopwatchSupplier) { - this.targetName = checkNotNull(targetName, "targetName"); - this.xdsChannel = checkNotNull(channel, "channel"); - this.node = checkNotNull(node, "node"); - this.syncContext = checkNotNull(syncContext, "syncContext"); - this.timeService = checkNotNull(timeService, "timeService"); - this.backoffPolicyProvider = checkNotNull(backoffPolicyProvider, "backoffPolicyProvider"); - this.stopwatchSupplier = checkNotNull(stopwatchSupplier, "stopwatch"); - adsStreamRetryStopwatch = stopwatchSupplier.get(); - logId = InternalLogId.allocate("xds-client", targetName); - logger = XdsLogger.withLogId(logId); - logger.log(XdsLogLevel.INFO, "Created"); - } - - @Override - void shutdown() { - logger.log(XdsLogLevel.INFO, "Shutting down"); - xdsChannel.getManagedChannel().shutdown(); - if (adsStream != null) { - adsStream.close(Status.CANCELLED.withDescription("shutdown").asException()); - } - cleanUpResourceTimers(); - if (lrsClient != null) { - lrsClient.stopLoadReporting(); - lrsClient = null; - } - if (rpcRetryTimer != null) { - rpcRetryTimer.cancel(); - } - } - - private void cleanUpResourceTimers() { - if (ldsRespTimer != null) { - ldsRespTimer.cancel(); - ldsRespTimer = null; - } - if (rdsRespTimer != null) { - rdsRespTimer.cancel(); - rdsRespTimer = null; - } - for (ResourceSubscriber subscriber : cdsResourceSubscribers.values()) { - subscriber.stopTimer(); - } - for (ResourceSubscriber subscriber : edsResourceSubscribers.values()) { - subscriber.stopTimer(); - } - } - - @Override - void watchConfigData(String targetAuthority, ConfigWatcher watcher) { - checkState(configWatcher == null, "watcher for %s already registered", targetAuthority); - checkState(listenerWatcher == null, "ListenerWatcher already registered"); - ldsResourceName = checkNotNull(targetAuthority, "targetAuthority"); - configWatcher = checkNotNull(watcher, "watcher"); - logger.log(XdsLogLevel.INFO, "Started watching config {0}", ldsResourceName); - if (rpcRetryTimer != null && rpcRetryTimer.isPending()) { - // Currently in retry backoff. - return; - } - if (adsStream == null) { - startRpcStream(); - } - adsStream.sendXdsRequest(ResourceType.LDS, ImmutableList.of(ldsResourceName)); - ldsRespTimer = - syncContext - .schedule( - new LdsResourceFetchTimeoutTask(ldsResourceName), - INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS, timeService); - } - - @Override - void watchCdsResource(String resourceName, CdsResourceWatcher watcher) { - ResourceSubscriber subscriber = cdsResourceSubscribers.get(resourceName); - if (subscriber == null) { - logger.log(XdsLogLevel.INFO, "Subscribe CDS resource {0}", resourceName); - subscriber = new ResourceSubscriber(ResourceType.CDS, resourceName); - cdsResourceSubscribers.put(resourceName, subscriber); - adjustResourceSubscription(ResourceType.CDS, cdsResourceSubscribers.keySet()); - } - subscriber.addWatcher(watcher); - } - - @Override - void cancelCdsResourceWatch(String resourceName, CdsResourceWatcher watcher) { - ResourceSubscriber subscriber = cdsResourceSubscribers.get(resourceName); - subscriber.removeWatcher(watcher); - if (!subscriber.isWatched()) { - subscriber.stopTimer(); - logger.log(XdsLogLevel.INFO, "Unsubscribe CDS resource {0}", resourceName); - cdsResourceSubscribers.remove(resourceName); - adjustResourceSubscription(ResourceType.CDS, cdsResourceSubscribers.keySet()); - } - } - - @Override - void watchEdsResource(String resourceName, EdsResourceWatcher watcher) { - ResourceSubscriber subscriber = edsResourceSubscribers.get(resourceName); - if (subscriber == null) { - logger.log(XdsLogLevel.INFO, "Subscribe EDS resource {0}", resourceName); - subscriber = new ResourceSubscriber(ResourceType.EDS, resourceName); - edsResourceSubscribers.put(resourceName, subscriber); - adjustResourceSubscription(ResourceType.EDS, edsResourceSubscribers.keySet()); - } - subscriber.addWatcher(watcher); - } - - @Override - void cancelEdsResourceWatch(String resourceName, EdsResourceWatcher watcher) { - ResourceSubscriber subscriber = edsResourceSubscribers.get(resourceName); - subscriber.removeWatcher(watcher); - if (!subscriber.isWatched()) { - subscriber.stopTimer(); - logger.log(XdsLogLevel.INFO, "Unsubscribe EDS resource {0}", resourceName); - edsResourceSubscribers.remove(resourceName); - adjustResourceSubscription(ResourceType.EDS, edsResourceSubscribers.keySet()); - } - } - - @Override - void watchListenerData(int port, ListenerWatcher watcher) { - checkState(configWatcher == null, - "ListenerWatcher cannot be set when ConfigWatcher set"); - checkState(listenerWatcher == null, "ListenerWatcher already registered"); - listenerWatcher = checkNotNull(watcher, "watcher"); - checkArgument(port > 0, "port needs to be > 0"); - this.listenerPort = port; - logger.log(XdsLogLevel.INFO, "Started watching listener for port {0}", port); - if (rpcRetryTimer != null && rpcRetryTimer.isPending()) { - // Currently in retry backoff. - return; - } - if (adsStream == null) { - startRpcStream(); - } - updateNodeMetadataForListenerRequest(port); - adsStream.sendXdsRequest(ResourceType.LDS, ImmutableList.of()); - ldsRespTimer = - syncContext - .schedule( - new ListenerResourceFetchTimeoutTask(":" + port), - INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS, timeService); - } - - /** In case of Listener watcher metadata to be updated to include port. */ - private void updateNodeMetadataForListenerRequest(int port) { - Map newMetadata = new HashMap<>(); - if (node.getMetadata() != null) { - newMetadata.putAll(node.getMetadata()); - } - newMetadata.put("TRAFFICDIRECTOR_PROXYLESS", "1"); - // TODO(sanjaypujare): eliminate usage of listening_addresses. - EnvoyProtoData.Address listeningAddress = - new EnvoyProtoData.Address("0.0.0.0", port); - node = - node.toBuilder().setMetadata(newMetadata).addListeningAddresses(listeningAddress).build(); - } - - @Override - void reportClientStats() { - if (lrsClient == null) { - logger.log(XdsLogLevel.INFO, "Turning on load reporting"); - lrsClient = - new LoadReportClient( - targetName, - loadStatsManager, - xdsChannel, - node, - syncContext, - timeService, - backoffPolicyProvider, - stopwatchSupplier); - } - if (loadReportCount == 0) { - lrsClient.startLoadReporting(); - } - loadReportCount++; - } - - @Override - void cancelClientStatsReport() { - checkState(loadReportCount > 0, "load reporting was never started"); - loadReportCount--; - if (loadReportCount == 0) { - logger.log(XdsLogLevel.INFO, "Turning off load reporting"); - lrsClient.stopLoadReporting(); - lrsClient = null; - } - } - - @Override - LoadStatsStore addClientStats(String clusterName, @Nullable String clusterServiceName) { - return loadStatsManager.addLoadStats(clusterName, clusterServiceName); - } - - @Override - void removeClientStats(String clusterName, @Nullable String clusterServiceName) { - loadStatsManager.removeLoadStats(clusterName, clusterServiceName); - } - - @Override - public String toString() { - return logId.toString(); - } - - /** - * Establishes the RPC connection by creating a new RPC stream on the given channel for - * xDS protocol communication. - */ - private void startRpcStream() { - checkState(adsStream == null, "Previous adsStream has not been cleared yet"); - if (xdsChannel.isUseProtocolV3()) { - adsStream = new AdsStream(); - } else { - adsStream = new AdsStreamV2(); - } - adsStream.start(); - logger.log(XdsLogLevel.INFO, "ADS stream started"); - adsStreamRetryStopwatch.reset().start(); - } - - /** - * Calls handleLdsResponseForListener or handleLdsResponseForConfigUpdate based on which watcher - * was set. - */ - private void handleLdsResponse(DiscoveryResponseData ldsResponse) { - checkState((configWatcher != null) != (listenerWatcher != null), - "No LDS request was ever sent. Management server is doing something wrong"); - if (listenerWatcher != null) { - handleLdsResponseForListener(ldsResponse); - } else { - handleLdsResponseForConfigUpdate(ldsResponse); - } - } - - /** - * Handles LDS response to find the HttpConnectionManager message for the requested resource name. - * Proceed with the resolved RouteConfiguration in HttpConnectionManager message of the requested - * listener, if exists, to find the VirtualHost configuration for the "xds:" URI - * (with the port, if any, stripped off). Or sends an RDS request if configured for dynamic - * resolution. The response is NACKed if contains invalid data for gRPC's usage. Otherwise, an - * ACK request is sent to management server. - */ - private void handleLdsResponseForConfigUpdate(DiscoveryResponseData ldsResponse) { - checkState(ldsResourceName != null && configWatcher != null, - "LDS request for ConfigWatcher was never sent!"); - - // Unpack Listener messages. - List listeners = new ArrayList<>(ldsResponse.getResourcesList().size()); - List listenerNames = new ArrayList<>(ldsResponse.getResourcesList().size()); - try { - for (com.google.protobuf.Any res : ldsResponse.getResourcesList()) { - if (res.getTypeUrl().equals(ADS_TYPE_URL_LDS_V2)) { - res = res.toBuilder().setTypeUrl(ADS_TYPE_URL_LDS).build(); - } - Listener listener = res.unpack(Listener.class); - listeners.add(listener); - listenerNames.add(listener.getName()); - } - } catch (InvalidProtocolBufferException e) { - logger.log(XdsLogLevel.WARNING, "Failed to unpack Listeners in LDS response {0}", e); - adsStream.sendNackRequest( - ResourceType.LDS, ImmutableList.of(ldsResourceName), - ldsResponse.getVersionInfo(), "Malformed LDS response: " + e); - return; - } - logger.log(XdsLogLevel.INFO, "Received LDS response for resources: {0}", listenerNames); - - // Unpack HttpConnectionManager messages. - HttpConnectionManager requestedHttpConnManager = null; - try { - for (Listener listener : listeners) { - Any apiListener = listener.getApiListener().getApiListener(); - if (apiListener.getTypeUrl().equals(TYPE_URL_HTTP_CONNECTION_MANAGER_V2)) { - apiListener = - apiListener.toBuilder().setTypeUrl(TYPE_URL_HTTP_CONNECTION_MANAGER).build(); - } - HttpConnectionManager hm = apiListener.unpack(HttpConnectionManager.class); - if (listener.getName().equals(ldsResourceName)) { - requestedHttpConnManager = hm; - } - } - } catch (InvalidProtocolBufferException e) { - logger.log( - XdsLogLevel.WARNING, - "Failed to unpack HttpConnectionManagers in Listeners of LDS response {0}", e); - adsStream.sendNackRequest( - ResourceType.LDS, ImmutableList.of(ldsResourceName), - ldsResponse.getVersionInfo(), "Malformed LDS response: " + e); - return; - } - - String errorMessage = null; - // Routes found in the in-lined RouteConfiguration, if exists. - List routes = null; - // RouteConfiguration name to be used as the resource name for RDS request, if exists. - String rdsRouteConfigName = null; - // Process the requested Listener if exists, either extract cluster information from in-lined - // RouteConfiguration message or send an RDS request for dynamic resolution. - if (requestedHttpConnManager != null) { - logger.log(XdsLogLevel.DEBUG, "Found http connection manager"); - // The HttpConnectionManager message must either provide the RouteConfiguration directly - // in-line or tell the client to use RDS to obtain it. - // TODO(chengyuanzhang): if both route_config and rds are set, it should be either invalid - // data or one supersedes the other. TBD. - if (requestedHttpConnManager.hasRouteConfig()) { - RouteConfiguration rc = requestedHttpConnManager.getRouteConfig(); - try { - routes = findRoutesInRouteConfig(rc, ldsResourceName); - } catch (InvalidProtoDataException e) { - errorMessage = - "Listener " + ldsResourceName + " : cannot find a valid cluster name in any " - + "virtual hosts domains matching: " + ldsResourceName - + " with the reason : " + e.getMessage(); - } - } else if (requestedHttpConnManager.hasRds()) { - Rds rds = requestedHttpConnManager.getRds(); - if (!rds.getConfigSource().hasAds()) { - errorMessage = - "Listener " + ldsResourceName + " : for using RDS, config_source must be " - + "set to use ADS."; - } else { - rdsRouteConfigName = rds.getRouteConfigName(); - } - } else { - errorMessage = "Listener " + ldsResourceName + " : HttpConnectionManager message must " - + "either provide the RouteConfiguration directly in-line or tell the client to " - + "use RDS to obtain it."; - } - } - - if (errorMessage != null) { - adsStream.sendNackRequest( - ResourceType.LDS, ImmutableList.of(ldsResourceName), - ldsResponse.getVersionInfo(), errorMessage); - return; - } - adsStream.sendAckRequest(ResourceType.LDS, ImmutableList.of(ldsResourceName), - ldsResponse.getVersionInfo()); - - if (routes != null || rdsRouteConfigName != null) { - if (ldsRespTimer != null) { - ldsRespTimer.cancel(); - ldsRespTimer = null; - } - } - if (routes != null) { - // Found routes in the in-lined RouteConfiguration. - logger.log( - XdsLogLevel.DEBUG, - "Found routes (inlined in route config): {0}", routes); - ConfigUpdate configUpdate = ConfigUpdate.newBuilder().addRoutes(routes).build(); - configWatcher.onConfigChanged(configUpdate); - } else if (rdsRouteConfigName != null) { - // Send an RDS request if the resource to request has changed. - if (!rdsRouteConfigName.equals(adsStream.rdsResourceName)) { - logger.log( - XdsLogLevel.INFO, - "Use RDS to dynamically resolve route config, resource name: {0}", rdsRouteConfigName); - adsStream.sendXdsRequest(ResourceType.RDS, ImmutableList.of(rdsRouteConfigName)); - // Cancel the timer for fetching the previous RDS resource. - if (rdsRespTimer != null) { - rdsRespTimer.cancel(); - } - rdsRespTimer = - syncContext - .schedule( - new RdsResourceFetchTimeoutTask(rdsRouteConfigName), - INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS, timeService); - } - } else { - // The requested Listener is removed by management server. - if (ldsRespTimer == null) { - configWatcher.onResourceDoesNotExist(ldsResourceName); - } - } - } - - private void handleLdsResponseForListener(DiscoveryResponseData ldsResponse) { - checkState(ldsResourceName == null && listenerPort > 0 && listenerWatcher != null, - "LDS request for ListenerWatcher was never sent!"); - - // Unpack Listener messages. - Listener requestedListener = null; - logger.log(XdsLogLevel.DEBUG, "Listener count: {0}", ldsResponse.getResourcesList().size()); - try { - for (com.google.protobuf.Any res : ldsResponse.getResourcesList()) { - if (res.getTypeUrl().equals(ADS_TYPE_URL_LDS_V2)) { - res = res.toBuilder().setTypeUrl(ADS_TYPE_URL_LDS).build(); - } - Listener listener = res.unpack(Listener.class); - logger.log(XdsLogLevel.DEBUG, "Found listener {0}", listener.toString()); - if (isRequestedListener(listener)) { - requestedListener = listener; - logger.log(XdsLogLevel.DEBUG, "Requested listener found: {0}", listener.getName()); - } - } - } catch (InvalidProtocolBufferException e) { - logger.log(XdsLogLevel.WARNING, "Failed to unpack Listeners in LDS response {0}", e); - adsStream.sendNackRequest( - ResourceType.LDS, ImmutableList.of(), - ldsResponse.getVersionInfo(), "Malformed LDS response: " + e); - return; - } - ListenerUpdate listenerUpdate = null; - if (requestedListener != null) { - if (ldsRespTimer != null) { - ldsRespTimer.cancel(); - ldsRespTimer = null; - } - try { - listenerUpdate = ListenerUpdate.newBuilder() - .setListener(EnvoyServerProtoData.Listener.fromEnvoyProtoListener(requestedListener)) - .build(); - } catch (InvalidProtocolBufferException e) { - logger.log(XdsLogLevel.WARNING, "Failed to unpack Listener in LDS response {0}", e); - adsStream.sendNackRequest( - ResourceType.LDS, ImmutableList.of(), - ldsResponse.getVersionInfo(), "Malformed LDS response: " + e); - return; - } - } else { - if (ldsRespTimer == null) { - listenerWatcher.onResourceDoesNotExist(":" + listenerPort); - } - } - adsStream.sendAckRequest(ResourceType.LDS, ImmutableList.of(), - ldsResponse.getVersionInfo()); - if (listenerUpdate != null) { - listenerWatcher.onListenerChanged(listenerUpdate); - } - } - - private boolean isRequestedListener(Listener listener) { - // TODO(sanjaypujare): check listener.getName() once we know what xDS server returns - return isAddressMatching(listener.getAddress()) - && hasMatchingFilter(listener.getFilterChainsList()); - } - - private boolean isAddressMatching(Address address) { - // TODO(sanjaypujare): check IP address once we know xDS server will include it - return address.hasSocketAddress() - && (address.getSocketAddress().getPortValue() == listenerPort); - } - - private boolean hasMatchingFilter(List filterChainsList) { - // TODO(sanjaypujare): if myIp to be checked against filterChainMatch.getPrefixRangesList() - for (FilterChain filterChain : filterChainsList) { - FilterChainMatch filterChainMatch = filterChain.getFilterChainMatch(); - - if (listenerPort == filterChainMatch.getDestinationPort().getValue()) { - return true; - } - } - return false; - } - - /** - * Handles RDS response to find the RouteConfiguration message for the requested resource name. - * Proceed with the resolved RouteConfiguration if exists to find the VirtualHost configuration - * for the "xds:" URI (with the port, if any, stripped off). The response is NACKed if contains - * invalid data for gRPC's usage. Otherwise, an ACK request is sent to management server. - */ - private void handleRdsResponse(DiscoveryResponseData rdsResponse) { - checkState(adsStream.rdsResourceName != null, - "Never requested for RDS resources, management server is doing something wrong"); - - // Unpack RouteConfiguration messages. - List routeConfigNames = new ArrayList<>(rdsResponse.getResourcesList().size()); - RouteConfiguration requestedRouteConfig = null; - try { - for (com.google.protobuf.Any res : rdsResponse.getResourcesList()) { - if (res.getTypeUrl().equals(ADS_TYPE_URL_RDS_V2)) { - res = res.toBuilder().setTypeUrl(ADS_TYPE_URL_RDS).build(); - } - RouteConfiguration rc = res.unpack(RouteConfiguration.class); - routeConfigNames.add(rc.getName()); - if (rc.getName().equals(adsStream.rdsResourceName)) { - requestedRouteConfig = rc; - } - } - } catch (InvalidProtocolBufferException e) { - logger.log( - XdsLogLevel.WARNING, "Failed to unpack RouteConfiguration in RDS response {0}", e); - adsStream.sendNackRequest( - ResourceType.RDS, ImmutableList.of(adsStream.rdsResourceName), - rdsResponse.getVersionInfo(), "Malformed RDS response: " + e); - return; - } - logger.log( - XdsLogLevel.INFO, "Received RDS response for resources: {0}", routeConfigNames); - - // Resolved cluster name for the requested resource, if exists. - List routes = null; - if (requestedRouteConfig != null) { - try { - routes = findRoutesInRouteConfig(requestedRouteConfig, ldsResourceName); - } catch (InvalidProtoDataException e) { - String errorDetail = e.getMessage(); - adsStream.sendNackRequest( - ResourceType.RDS, ImmutableList.of(adsStream.rdsResourceName), - rdsResponse.getVersionInfo(), - "RouteConfiguration " + requestedRouteConfig.getName() + ": cannot find a " - + "valid cluster name in any virtual hosts with domains matching: " - + ldsResourceName - + " with the reason: " + errorDetail); - return; - } - } - - adsStream.sendAckRequest(ResourceType.RDS, ImmutableList.of(adsStream.rdsResourceName), - rdsResponse.getVersionInfo()); - - // Notify the ConfigWatcher if this RDS response contains the most recently requested - // RDS resource. - if (routes != null) { - if (rdsRespTimer != null) { - rdsRespTimer.cancel(); - rdsRespTimer = null; - } - logger.log(XdsLogLevel.DEBUG, "Found routes: {0}", routes); - ConfigUpdate configUpdate = - ConfigUpdate.newBuilder().addRoutes(routes).build(); - configWatcher.onConfigChanged(configUpdate); - } - } - - /** - * Processes a RouteConfiguration message to find the routes that requests for the given host will - * be routed to. - * - * @throws InvalidProtoDataException if the message contains invalid data. - */ - private static List findRoutesInRouteConfig( - RouteConfiguration config, String hostName) throws InvalidProtoDataException { - VirtualHost targetVirtualHost = findVirtualHostForHostName(config, hostName); - if (targetVirtualHost == null) { - throw new InvalidProtoDataException("Unable to find virtual host for " + hostName); - } - - // Note we would consider upstream cluster not found if the virtual host is not configured - // correctly for gRPC, even if there exist other virtual hosts with (lower priority) - // matching domains. - return populateRoutesInVirtualHost(targetVirtualHost); - } - - @VisibleForTesting - static List populateRoutesInVirtualHost(VirtualHost virtualHost) - throws InvalidProtoDataException { - List routes = new ArrayList<>(); - List routesProto = virtualHost.getRoutesList(); - for (Route routeProto : routesProto) { - StructOrError route = - EnvoyProtoData.Route.fromEnvoyProtoRoute(routeProto); - if (route == null) { - continue; - } else if (route.getErrorDetail() != null) { - throw new InvalidProtoDataException( - "Virtual host [" + virtualHost.getName() + "] contains invalid route : " - + route.getErrorDetail()); - } - routes.add(route.getStruct()); - } - if (routes.isEmpty()) { - throw new InvalidProtoDataException( - "Virtual host [" + virtualHost.getName() + "] contains no usable route"); - } - return Collections.unmodifiableList(routes); - } - - @VisibleForTesting - @Nullable - static VirtualHost findVirtualHostForHostName( - RouteConfiguration config, String hostName) { - List virtualHosts = config.getVirtualHostsList(); - // Domain search order: - // 1. Exact domain names: ``www.foo.com``. - // 2. Suffix domain wildcards: ``*.foo.com`` or ``*-bar.foo.com``. - // 3. Prefix domain wildcards: ``foo.*`` or ``foo-*``. - // 4. Special wildcard ``*`` matching any domain. - // - // The longest wildcards match first. - // Assuming only a single virtual host in the entire route configuration can match - // on ``*`` and a domain must be unique across all virtual hosts. - int matchingLen = -1; // longest length of wildcard pattern that matches host name - boolean exactMatchFound = false; // true if a virtual host with exactly matched domain found - VirtualHost targetVirtualHost = null; // target VirtualHost with longest matched domain - for (VirtualHost vHost : virtualHosts) { - for (String domain : vHost.getDomainsList()) { - boolean selected = false; - if (matchHostName(hostName, domain)) { // matching - if (!domain.contains("*")) { // exact matching - exactMatchFound = true; - targetVirtualHost = vHost; - break; - } else if (domain.length() > matchingLen) { // longer matching pattern - selected = true; - } else if (domain.length() == matchingLen && domain.startsWith("*")) { // suffix matching - selected = true; - } - } - if (selected) { - matchingLen = domain.length(); - targetVirtualHost = vHost; - } - } - if (exactMatchFound) { - break; - } - } - return targetVirtualHost; - } - - /** - * Handles CDS response, which contains a list of Cluster messages with information for a logical - * cluster. The response is NACKed if messages for requested resources contain invalid - * information for gRPC's usage. Otherwise, an ACK request is sent to management server. - * Response data for requested clusters is cached locally, in case of new cluster watchers - * interested in the same clusters are added later. - */ - private void handleCdsResponse(DiscoveryResponseData cdsResponse) { - adsStream.cdsRespNonce = cdsResponse.getNonce(); - - // Unpack Cluster messages. - List clusters = new ArrayList<>(cdsResponse.getResourcesList().size()); - List clusterNames = new ArrayList<>(cdsResponse.getResourcesList().size()); - try { - for (com.google.protobuf.Any res : cdsResponse.getResourcesList()) { - if (res.getTypeUrl().equals(ADS_TYPE_URL_CDS_V2)) { - res = res.toBuilder().setTypeUrl(ADS_TYPE_URL_CDS).build(); - } - Cluster cluster = res.unpack(Cluster.class); - clusters.add(cluster); - clusterNames.add(cluster.getName()); - } - } catch (InvalidProtocolBufferException e) { - logger.log(XdsLogLevel.WARNING, "Failed to unpack Clusters in CDS response {0}", e); - adsStream.sendNackRequest( - ResourceType.CDS, cdsResourceSubscribers.keySet(), - cdsResponse.getVersionInfo(), "Malformed CDS response: " + e); - return; - } - logger.log(XdsLogLevel.INFO, "Received CDS response for resources: {0}", clusterNames); - - String errorMessage = null; - // Cluster information update for requested clusters received in this CDS response. - Map cdsUpdates = new HashMap<>(); - // CDS responses represents the state of the world, EDS services not referenced by - // Clusters are those no longer exist. - Set edsServices = new HashSet<>(); - for (Cluster cluster : clusters) { - String clusterName = cluster.getName(); - // Skip information for clusters not requested. - // Management server is required to always send newly requested resources, even if they - // may have been sent previously (proactively). Thus, client does not need to cache - // unrequested resources. - if (!cdsResourceSubscribers.containsKey(clusterName)) { - continue; - } - CdsUpdate.Builder updateBuilder = CdsUpdate.newBuilder(); - updateBuilder.setClusterName(clusterName); - // The type field must be set to EDS. - if (!cluster.getType().equals(DiscoveryType.EDS)) { - errorMessage = "Cluster " + clusterName + " : only EDS discovery type is supported " - + "in gRPC."; - break; - } - // In the eds_cluster_config field, the eds_config field must be set to indicate to - // use EDS (must be set to use ADS). - EdsClusterConfig edsClusterConfig = cluster.getEdsClusterConfig(); - if (!edsClusterConfig.getEdsConfig().hasAds()) { - errorMessage = "Cluster " + clusterName + " : field eds_cluster_config must be set to " - + "indicate to use EDS over ADS."; - break; - } - // If the service_name field is set, that value will be used for the EDS request. - if (!edsClusterConfig.getServiceName().isEmpty()) { - updateBuilder.setEdsServiceName(edsClusterConfig.getServiceName()); - edsServices.add(edsClusterConfig.getServiceName()); - } else { - edsServices.add(clusterName); - } - // The lb_policy field must be set to ROUND_ROBIN. - if (!cluster.getLbPolicy().equals(LbPolicy.ROUND_ROBIN)) { - errorMessage = "Cluster " + clusterName + " : only round robin load balancing policy is " - + "supported in gRPC."; - break; - } - updateBuilder.setLbPolicy("round_robin"); - // If the lrs_server field is set, it must have its self field set, in which case the - // client should use LRS for load reporting. Otherwise (the lrs_server field is not set), - // LRS load reporting will be disabled. - if (cluster.hasLrsServer()) { - if (!cluster.getLrsServer().hasSelf()) { - errorMessage = "Cluster " + clusterName + " : only support enabling LRS for the same " - + "management server."; - break; - } - updateBuilder.setLrsServerName(""); - } - try { - UpstreamTlsContext upstreamTlsContext = getTlsContextFromCluster(cluster); - if (upstreamTlsContext != null && upstreamTlsContext.getCommonTlsContext() != null) { - updateBuilder.setUpstreamTlsContext(upstreamTlsContext); - } - } catch (InvalidProtocolBufferException e) { - errorMessage = "Cluster " + clusterName + " : " + e.getMessage(); - break; - } - cdsUpdates.put(clusterName, updateBuilder.build()); - } - if (errorMessage != null) { - adsStream.sendNackRequest( - ResourceType.CDS, - cdsResourceSubscribers.keySet(), - cdsResponse.getVersionInfo(), - errorMessage); - return; - } - adsStream.sendAckRequest(ResourceType.CDS, cdsResourceSubscribers.keySet(), - cdsResponse.getVersionInfo()); - - for (String resource : cdsResourceSubscribers.keySet()) { - ResourceSubscriber subscriber = cdsResourceSubscribers.get(resource); - if (cdsUpdates.containsKey(resource)) { - subscriber.onData(cdsUpdates.get(resource)); - } else { - subscriber.onAbsent(); - } - } - for (String resource : edsResourceSubscribers.keySet()) { - if (!edsServices.contains(resource)) { - ResourceSubscriber subscriber = edsResourceSubscribers.get(resource); - subscriber.onAbsent(); - } - } - } - - @Nullable - private static UpstreamTlsContext getTlsContextFromCluster(Cluster cluster) - throws InvalidProtocolBufferException { - if (cluster.hasTransportSocket() && "tls".equals(cluster.getTransportSocket().getName())) { - Any any = cluster.getTransportSocket().getTypedConfig(); - return UpstreamTlsContext.fromEnvoyProtoUpstreamTlsContext( - io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext.parseFrom( - any.getValue())); - } - return null; - } - - /** - * Handles EDS response, which contains a list of ClusterLoadAssignment messages with - * endpoint load balancing information for each cluster. The response is NACKed if messages - * for requested resources contain invalid information for gRPC's usage. Otherwise, - * an ACK request is sent to management server. Response data for requested clusters is - * cached locally, in case of new endpoint watchers interested in the same clusters - * are added later. - */ - private void handleEdsResponse(DiscoveryResponseData edsResponse) { - // Unpack ClusterLoadAssignment messages. - List clusterLoadAssignments = - new ArrayList<>(edsResponse.getResourcesList().size()); - List claNames = new ArrayList<>(edsResponse.getResourcesList().size()); - try { - for (com.google.protobuf.Any res : edsResponse.getResourcesList()) { - if (res.getTypeUrl().equals(ADS_TYPE_URL_EDS_V2)) { - res = res.toBuilder().setTypeUrl(ADS_TYPE_URL_EDS).build(); - } - ClusterLoadAssignment assignment = res.unpack(ClusterLoadAssignment.class); - clusterLoadAssignments.add(assignment); - claNames.add(assignment.getClusterName()); - } - } catch (InvalidProtocolBufferException e) { - logger.log( - XdsLogLevel.WARNING, "Failed to unpack ClusterLoadAssignments in EDS response {0}", e); - adsStream.sendNackRequest( - ResourceType.EDS, edsResourceSubscribers.keySet(), - edsResponse.getVersionInfo(), "Malformed EDS response: " + e); - return; - } - logger.log(XdsLogLevel.INFO, "Received EDS response for resources: {0}", claNames); - - String errorMessage = null; - // Endpoint information updates for requested clusters received in this EDS response. - Map edsUpdates = new HashMap<>(); - // Walk through each ClusterLoadAssignment message. If any of them for requested clusters - // contain invalid information for gRPC's load balancing usage, the whole response is rejected. - for (ClusterLoadAssignment assignment : clusterLoadAssignments) { - String clusterName = assignment.getClusterName(); - // Skip information for clusters not requested. - // Management server is required to always send newly requested resources, even if they - // may have been sent previously (proactively). Thus, client does not need to cache - // unrequested resources. - if (!edsResourceSubscribers.containsKey(clusterName)) { - continue; - } - EdsUpdate.Builder updateBuilder = EdsUpdate.newBuilder(); - updateBuilder.setClusterName(clusterName); - Set priorities = new HashSet<>(); - int maxPriority = -1; - for (io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints localityLbEndpoints - : assignment.getEndpointsList()) { - // Filter out localities without or with 0 weight. - if (!localityLbEndpoints.hasLoadBalancingWeight() - || localityLbEndpoints.getLoadBalancingWeight().getValue() < 1) { - continue; - } - int localityPriority = localityLbEndpoints.getPriority(); - if (localityPriority < 0) { - errorMessage = - "ClusterLoadAssignment " + clusterName + " : locality with negative priority."; - break; - } - maxPriority = Math.max(maxPriority, localityPriority); - priorities.add(localityPriority); - // The endpoint field of each lb_endpoints must be set. - // Inside of it: the address field must be set. - for (LbEndpoint lbEndpoint : localityLbEndpoints.getLbEndpointsList()) { - if (!lbEndpoint.getEndpoint().hasAddress()) { - errorMessage = "ClusterLoadAssignment " + clusterName + " : endpoint with no address."; - break; - } - } - if (errorMessage != null) { - break; - } - // Note endpoints with health status other than UNHEALTHY and UNKNOWN are still - // handed over to watching parties. It is watching parties' responsibility to - // filter out unhealthy endpoints. See EnvoyProtoData.LbEndpoint#isHealthy(). - updateBuilder.addLocalityLbEndpoints( - Locality.fromEnvoyProtoLocality(localityLbEndpoints.getLocality()), - LocalityLbEndpoints.fromEnvoyProtoLocalityLbEndpoints(localityLbEndpoints)); - } - if (errorMessage != null) { - break; - } - if (priorities.size() != maxPriority + 1) { - errorMessage = "ClusterLoadAssignment " + clusterName + " : sparse priorities."; - break; - } - for (ClusterLoadAssignment.Policy.DropOverload dropOverload - : assignment.getPolicy().getDropOverloadsList()) { - updateBuilder.addDropPolicy(DropOverload.fromEnvoyProtoDropOverload(dropOverload)); - } - EdsUpdate update = updateBuilder.build(); - edsUpdates.put(clusterName, update); - } - if (errorMessage != null) { - adsStream.sendNackRequest( - ResourceType.EDS, - edsResourceSubscribers.keySet(), - edsResponse.getVersionInfo(), - errorMessage); - return; - } - adsStream.sendAckRequest(ResourceType.EDS, edsResourceSubscribers.keySet(), - edsResponse.getVersionInfo()); - - for (String resource : edsResourceSubscribers.keySet()) { - ResourceSubscriber subscriber = edsResourceSubscribers.get(resource); - if (edsUpdates.containsKey(resource)) { - subscriber.onData(edsUpdates.get(resource)); - } - } - } - - private void adjustResourceSubscription(ResourceType type, Collection resources) { - if (rpcRetryTimer != null && rpcRetryTimer.isPending()) { - // Currently in retry backoff. - return; - } - if (adsStream == null) { - startRpcStream(); - } - adsStream.sendXdsRequest(type, resources); - } - - @VisibleForTesting - final class RpcRetryTask implements Runnable { - @Override - public void run() { - startRpcStream(); - if (configWatcher != null) { - adsStream.sendXdsRequest(ResourceType.LDS, ImmutableList.of(ldsResourceName)); - ldsRespTimer = - syncContext - .schedule( - new LdsResourceFetchTimeoutTask(ldsResourceName), - INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS, timeService); - } - if (listenerWatcher != null) { - adsStream.sendXdsRequest(ResourceType.LDS, ImmutableList.of()); - ldsRespTimer = - syncContext - .schedule( - new ListenerResourceFetchTimeoutTask(":" + listenerPort), - INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS, timeService); - } - if (!cdsResourceSubscribers.isEmpty()) { - adsStream.sendXdsRequest(ResourceType.CDS, cdsResourceSubscribers.keySet()); - for (ResourceSubscriber subscriber : cdsResourceSubscribers.values()) { - subscriber.restartTimer(); - } - } - if (!edsResourceSubscribers.isEmpty()) { - adsStream.sendXdsRequest(ResourceType.EDS, edsResourceSubscribers.keySet()); - for (ResourceSubscriber subscriber : edsResourceSubscribers.values()) { - subscriber.restartTimer(); - } - } - } - } - - @VisibleForTesting - enum ResourceType { - UNKNOWN, LDS, RDS, CDS, EDS; - - private String typeUrl() { - switch (this) { - case LDS: - return ADS_TYPE_URL_LDS; - case RDS: - return ADS_TYPE_URL_RDS; - case CDS: - return ADS_TYPE_URL_CDS; - case EDS: - return ADS_TYPE_URL_EDS; - case UNKNOWN: - default: - throw new AssertionError("Unknown or missing case in enum switch: " + this); - } - } - - private String typeUrlV2() { - switch (this) { - case LDS: - return ADS_TYPE_URL_LDS_V2; - case RDS: - return ADS_TYPE_URL_RDS_V2; - case CDS: - return ADS_TYPE_URL_CDS_V2; - case EDS: - return ADS_TYPE_URL_EDS_V2; - case UNKNOWN: - default: - throw new AssertionError("Unknown or missing case in enum switch: " + this); - } - } - - private static ResourceType fromTypeUrl(String typeUrl) { - switch (typeUrl) { - case ADS_TYPE_URL_LDS: - // fall trough - case ADS_TYPE_URL_LDS_V2: - return LDS; - case ADS_TYPE_URL_RDS: - // fall through - case ADS_TYPE_URL_RDS_V2: - return RDS; - case ADS_TYPE_URL_CDS: - // fall through - case ADS_TYPE_URL_CDS_V2: - return CDS; - case ADS_TYPE_URL_EDS: - // fall through - case ADS_TYPE_URL_EDS_V2: - return EDS; - default: - return UNKNOWN; - } - } - } - - /** - * Tracks a single subscribed resource. - */ - private final class ResourceSubscriber { - private final ResourceType type; - private final String resource; - private final Set watchers = new HashSet<>(); - // Resource states: - // - present: data != null; data is the cached data for the resource - // - absent: absent == true - // - unknown: anything else - // Note absent -> data == null, but not vice versa. - private ResourceUpdate data; - private boolean absent; - private ScheduledHandle respTimer; - - ResourceSubscriber(ResourceType type, String resource) { - this.type = type; - this.resource = resource; - if (rpcRetryTimer != null && rpcRetryTimer.isPending()) { - return; - } - restartTimer(); - } - - void addWatcher(ResourceWatcher watcher) { - checkArgument(!watchers.contains(watcher), "watcher %s already registered", watcher); - watchers.add(watcher); - if (data != null) { - notifyWatcher(watcher, data); - } else if (absent) { - watcher.onResourceDoesNotExist(resource); - } - } - - void removeWatcher(ResourceWatcher watcher) { - checkArgument(watchers.contains(watcher), "watcher %s not registered", watcher); - watchers.remove(watcher); - } - - void restartTimer() { - class ResourceNotFound implements Runnable { - @Override - public void run() { - respTimer = null; - onAbsent(); - } - - @Override - public String toString() { - return type + this.getClass().getSimpleName(); - } - } - - respTimer = syncContext.schedule( - new ResourceNotFound(), INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS, - timeService); - } - - void stopTimer() { - if (respTimer != null && respTimer.isPending()) { - respTimer.cancel(); - respTimer = null; - } - } - - boolean isWatched() { - return !watchers.isEmpty(); - } - - void onData(ResourceUpdate data) { - if (respTimer != null && respTimer.isPending()) { - respTimer.cancel(); - respTimer = null; - } - ResourceUpdate oldData = this.data; - this.data = data; - absent = false; - if (!Objects.equals(oldData, data)) { - for (ResourceWatcher watcher : watchers) { - notifyWatcher(watcher, data); - } - } - } - - void onAbsent() { - if (respTimer != null && respTimer.isPending()) { // too early to conclude absence - return; - } - if (!absent) { - data = null; - absent = true; - for (ResourceWatcher watcher : watchers) { - watcher.onResourceDoesNotExist(resource); - } - } - } - - void onError(Status error) { - if (respTimer != null && respTimer.isPending()) { - respTimer.cancel(); - respTimer = null; - } - for (ResourceWatcher watcher : watchers) { - watcher.onError(error); - } - } - - private void notifyWatcher(ResourceWatcher watcher, ResourceUpdate update) { - switch (type) { - case LDS: - ((LdsResourceWatcher) watcher).onChanged((LdsUpdate) update); - break; - case RDS: - ((RdsResourceWatcher) watcher).onChanged((RdsUpdate) update); - break; - case CDS: - ((CdsResourceWatcher) watcher).onChanged((CdsUpdate) update); - break; - case EDS: - ((EdsResourceWatcher) watcher).onChanged((EdsUpdate) update); - break; - case UNKNOWN: - default: - throw new AssertionError("should never be here"); - } - } - } - - private static final class DiscoveryRequestData { - private final ResourceType resourceType; - private final Collection resourceNames; - private final String versionInfo; - private final String responseNonce; - private final Node node; - @Nullable - private final com.google.rpc.Status errorDetail; - - DiscoveryRequestData( - ResourceType resourceType, Collection resourceNames, String versionInfo, - String responseNonce, Node node, @Nullable com.google.rpc.Status errorDetail) { - this.resourceType = resourceType; - this.resourceNames = resourceNames; - this.versionInfo = versionInfo; - this.responseNonce = responseNonce; - this.node = node; - this.errorDetail = errorDetail; - } - - DiscoveryRequest toEnvoyProto() { - DiscoveryRequest.Builder builder = - DiscoveryRequest.newBuilder() - .setVersionInfo(versionInfo) - .setNode(node.toEnvoyProtoNode()) - .addAllResourceNames(resourceNames) - .setTypeUrl(resourceType.typeUrl()) - .setResponseNonce(responseNonce); - if (errorDetail != null) { - builder.setErrorDetail(errorDetail); - } - return builder.build(); - } - - io.envoyproxy.envoy.api.v2.DiscoveryRequest toEnvoyProtoV2() { - io.envoyproxy.envoy.api.v2.DiscoveryRequest.Builder builder = - io.envoyproxy.envoy.api.v2.DiscoveryRequest.newBuilder() - .setVersionInfo(versionInfo) - .setNode(node.toEnvoyProtoNodeV2()) - .addAllResourceNames(resourceNames) - .setTypeUrl(resourceType.typeUrlV2()) - .setResponseNonce(responseNonce); - if (errorDetail != null) { - builder.setErrorDetail(errorDetail); - } - return builder.build(); - } - } - - private static final class DiscoveryResponseData { - private final ResourceType resourceType; - private final List resources; - private final String versionInfo; - private final String nonce; - - DiscoveryResponseData( - ResourceType resourceType, List resources, String versionInfo, String nonce) { - this.resourceType = resourceType; - this.resources = resources; - this.versionInfo = versionInfo; - this.nonce = nonce; - } - - ResourceType getResourceType() { - return resourceType; - } - - List getResourcesList() { - return resources; - } - - String getVersionInfo() { - return versionInfo; - } - - String getNonce() { - return nonce; - } - - static DiscoveryResponseData fromEnvoyProto(DiscoveryResponse proto) { - return new DiscoveryResponseData( - ResourceType.fromTypeUrl(proto.getTypeUrl()), proto.getResourcesList(), - proto.getVersionInfo(), proto.getNonce()); - } - - static DiscoveryResponseData fromEnvoyProtoV2( - io.envoyproxy.envoy.api.v2.DiscoveryResponse proto) { - return new DiscoveryResponseData( - ResourceType.fromTypeUrl(proto.getTypeUrl()), proto.getResourcesList(), - proto.getVersionInfo(), proto.getNonce()); - } - } - - private abstract class AbstractAdsStream { - private boolean responseReceived; - private boolean closed; - - // Response nonce for the most recently received discovery responses of each resource type. - // Client initiated requests start response nonce with empty string. - // A nonce is used to indicate the specific DiscoveryResponse each DiscoveryRequest - // corresponds to. - // A nonce becomes stale following a newer nonce being presented to the client in a - // DiscoveryResponse. - private String ldsRespNonce = ""; - private String rdsRespNonce = ""; - private String cdsRespNonce = ""; - private String edsRespNonce = ""; - - // Most recently requested RDS resource name, which is an intermediate resource name for - // resolving service config. - // LDS request always use the same resource name, which is the "xds:" URI. - // Resource names for EDS requests are always represented by the cluster names that - // watchers are interested in. - @Nullable - private String rdsResourceName; - - abstract void start(); - - abstract void sendDiscoveryRequest(DiscoveryRequestData request); - - abstract void sendError(Exception error); - - // Must run in syncContext. - final void handleResponse(DiscoveryResponseData response) { - if (closed) { - return; - } - responseReceived = true; - String respNonce = response.getNonce(); - // Nonce in each response is echoed back in the following ACK/NACK request. It is - // used for management server to identify which response the client is ACKing/NACking. - // To avoid confusion, client-initiated requests will always use the nonce in - // most recently received responses of each resource type. - ResourceType resourceType = response.getResourceType(); - switch (resourceType) { - case LDS: - ldsRespNonce = respNonce; - handleLdsResponse(response); - break; - case RDS: - rdsRespNonce = respNonce; - handleRdsResponse(response); - break; - case CDS: - cdsRespNonce = respNonce; - handleCdsResponse(response); - break; - case EDS: - edsRespNonce = respNonce; - handleEdsResponse(response); - break; - case UNKNOWN: - logger.log( - XdsLogLevel.WARNING, - "Received an unknown type of DiscoveryResponse\n{0}", - respNonce); - break; - default: - throw new AssertionError("Missing case in enum switch: " + resourceType); - } - } - - // Must run in syncContext. - final void handleRpcError(Throwable t) { - handleStreamClosed(Status.fromThrowable(t)); - } - - // Must run in syncContext. - final void handleRpcCompleted() { - handleStreamClosed(Status.UNAVAILABLE.withDescription("Closed by server")); - } - - private void handleStreamClosed(Status error) { - checkArgument(!error.isOk(), "unexpected OK status"); - if (closed) { - return; - } - logger.log( - XdsLogLevel.ERROR, - "ADS stream closed with status {0}: {1}. Cause: {2}", - error.getCode(), error.getDescription(), error.getCause()); - closed = true; - if (configWatcher != null) { - configWatcher.onError(error); - } - if (listenerWatcher != null) { - listenerWatcher.onError(error); - } - for (ResourceSubscriber subscriber : cdsResourceSubscribers.values()) { - subscriber.onError(error); - } - for (ResourceSubscriber subscriber : edsResourceSubscribers.values()) { - subscriber.onError(error); - } - cleanUp(); - cleanUpResourceTimers(); - if (responseReceived || retryBackoffPolicy == null) { - // Reset the backoff sequence if had received a response, or backoff sequence - // has never been initialized. - retryBackoffPolicy = backoffPolicyProvider.get(); - } - long delayNanos = 0; - if (!responseReceived) { - delayNanos = - Math.max( - 0, - retryBackoffPolicy.nextBackoffNanos() - - adsStreamRetryStopwatch.elapsed(TimeUnit.NANOSECONDS)); - } - logger.log(XdsLogLevel.INFO, "Retry ADS stream in {0} ns", delayNanos); - rpcRetryTimer = - syncContext.schedule( - new RpcRetryTask(), delayNanos, TimeUnit.NANOSECONDS, timeService); - } - - private void close(Exception error) { - if (closed) { - return; - } - closed = true; - cleanUp(); - sendError(error); - } - - private void cleanUp() { - if (adsStream == this) { - adsStream = null; - } - } - - /** - * Sends a DiscoveryRequest for the given resource name to management server. Memories the - * requested resource name (except for LDS as we always request for the singleton Listener) - * as we need it to find resources in responses. - */ - private void sendXdsRequest(ResourceType resourceType, Collection resourceNames) { - String version; - String nonce; - switch (resourceType) { - case LDS: - version = ldsVersion; - nonce = ldsRespNonce; - logger.log(XdsLogLevel.INFO, "Sending LDS request for resources: {0}", resourceNames); - break; - case RDS: - checkArgument( - resourceNames.size() == 1, "RDS request requesting for more than one resource"); - version = rdsVersion; - nonce = rdsRespNonce; - rdsResourceName = resourceNames.iterator().next(); - logger.log(XdsLogLevel.INFO, "Sending RDS request for resources: {0}", resourceNames); - break; - case CDS: - version = cdsVersion; - nonce = cdsRespNonce; - logger.log(XdsLogLevel.INFO, "Sending CDS request for resources: {0}", resourceNames); - break; - case EDS: - version = edsVersion; - nonce = edsRespNonce; - logger.log(XdsLogLevel.INFO, "Sending EDS request for resources: {0}", resourceNames); - break; - case UNKNOWN: - default: - throw new AssertionError("Unknown or missing case in enum switch: " + resourceType); - } - DiscoveryRequestData request = - new DiscoveryRequestData(resourceType, resourceNames, version, nonce, node, null); - sendDiscoveryRequest(request); - } - - /** - * Sends a DiscoveryRequest with the given information as an ACK. Updates the latest accepted - * version for the corresponding resource type. - */ - private void sendAckRequest(ResourceType resourceType, Collection resourceNames, - String versionInfo) { - String nonce; - switch (resourceType) { - case LDS: - ldsVersion = versionInfo; - nonce = ldsRespNonce; - logger.log(XdsLogLevel.WARNING, "Sending ACK for LDS update, version: {0}", versionInfo); - break; - case RDS: - rdsVersion = versionInfo; - nonce = rdsRespNonce; - logger.log(XdsLogLevel.WARNING, "Sending ACK for RDS update, version: {0}", versionInfo); - break; - case CDS: - cdsVersion = versionInfo; - nonce = cdsRespNonce; - logger.log(XdsLogLevel.WARNING, "Sending ACK for CDS update, version: {0}", versionInfo); - break; - case EDS: - edsVersion = versionInfo; - nonce = edsRespNonce; - logger.log(XdsLogLevel.WARNING, "Sending ACK for EDS update, version: {0}", versionInfo); - break; - case UNKNOWN: - default: - throw new AssertionError("Unknown or missing case in enum switch: " + resourceType); - } - DiscoveryRequestData request = - new DiscoveryRequestData(resourceType, resourceNames, versionInfo, nonce, node, null); - sendDiscoveryRequest(request); - } - - /** - * Sends a DiscoveryRequest with the given information as an NACK. NACK takes the previous - * accepted version. - */ - private void sendNackRequest(ResourceType resourceType, Collection resourceNames, - String rejectVersion, String message) { - String versionInfo; - String nonce; - switch (resourceType) { - case LDS: - versionInfo = ldsVersion; - nonce = ldsRespNonce; - logger.log( - XdsLogLevel.WARNING, - "Sending NACK for LDS update, version: {0}, reason: {1}", - rejectVersion, - message); - break; - case RDS: - versionInfo = rdsVersion; - nonce = rdsRespNonce; - logger.log( - XdsLogLevel.WARNING, - "Sending NACK for RDS update, version: {0}, reason: {1}", - rejectVersion, - message); - break; - case CDS: - versionInfo = cdsVersion; - nonce = cdsRespNonce; - logger.log( - XdsLogLevel.WARNING, - "Sending NACK for CDS update, version: {0}, reason: {1}", - rejectVersion, - message); - break; - case EDS: - versionInfo = edsVersion; - nonce = edsRespNonce; - logger.log( - XdsLogLevel.WARNING, - "Sending NACK for EDS update, version: {0}, reason: {1}", - rejectVersion, - message); - break; - case UNKNOWN: - default: - throw new AssertionError("Unknown or missing case in enum switch: " + resourceType); - } - com.google.rpc.Status error = com.google.rpc.Status.newBuilder() - .setCode(Code.INVALID_ARGUMENT_VALUE) - .setMessage(message) - .build(); - DiscoveryRequestData request = - new DiscoveryRequestData(resourceType, resourceNames, versionInfo, nonce, node, error); - sendDiscoveryRequest(request); - } - } - - private final class AdsStreamV2 extends AbstractAdsStream { - private final io.envoyproxy.envoy.service.discovery.v2.AggregatedDiscoveryServiceGrpc - .AggregatedDiscoveryServiceStub stubV2; - private StreamObserver requestWriterV2; - - AdsStreamV2() { - stubV2 = io.envoyproxy.envoy.service.discovery.v2.AggregatedDiscoveryServiceGrpc.newStub( - xdsChannel.getManagedChannel()); - } - - @Override - void start() { - StreamObserver responseReaderV2 = - new StreamObserver() { - @Override - public void onNext(final io.envoyproxy.envoy.api.v2.DiscoveryResponse response) { - syncContext.execute(new Runnable() { - @Override - public void run() { - if (logger.isLoggable(XdsLogLevel.DEBUG)) { - logger.log(XdsLogLevel.DEBUG, "Received {0} response:\n{1}", - ResourceType.fromTypeUrl(response.getTypeUrl()), - respPrinter.print(response)); - } - DiscoveryResponseData responseData = - DiscoveryResponseData.fromEnvoyProtoV2(response); - handleResponse(responseData); - } - }); - } - - @Override - public void onError(final Throwable t) { - syncContext.execute(new Runnable() { - @Override - public void run() { - handleRpcError(t); - } - }); - } - - @Override - public void onCompleted() { - syncContext.execute(new Runnable() { - @Override - public void run() { - handleRpcCompleted(); - } - }); - } - }; - requestWriterV2 = stubV2.withWaitForReady().streamAggregatedResources(responseReaderV2); - } - - @Override - void sendDiscoveryRequest(DiscoveryRequestData request) { - checkState(requestWriterV2 != null, "ADS stream has not been started"); - io.envoyproxy.envoy.api.v2.DiscoveryRequest requestProto = - request.toEnvoyProtoV2(); - requestWriterV2.onNext(requestProto); - logger.log(XdsLogLevel.DEBUG, "Sent DiscoveryRequest\n{0}", requestProto); - } - - @Override - void sendError(Exception error) { - requestWriterV2.onError(error); - } - } - - // AdsStream V3 - private final class AdsStream extends AbstractAdsStream { - private final AggregatedDiscoveryServiceGrpc.AggregatedDiscoveryServiceStub stub; - private StreamObserver requestWriter; - - AdsStream() { - stub = AggregatedDiscoveryServiceGrpc.newStub(xdsChannel.getManagedChannel()); - } - - @Override - void start() { - StreamObserver responseReader = new StreamObserver() { - @Override - public void onNext(final DiscoveryResponse response) { - syncContext.execute(new Runnable() { - @Override - public void run() { - if (logger.isLoggable(XdsLogLevel.DEBUG)) { - logger.log(XdsLogLevel.DEBUG, "Received {0} response:\n{1}", - ResourceType.fromTypeUrl(response.getTypeUrl()), respPrinter.print(response)); - } - DiscoveryResponseData responseData = DiscoveryResponseData.fromEnvoyProto(response); - handleResponse(responseData); - } - }); - } - - @Override - public void onError(final Throwable t) { - syncContext.execute(new Runnable() { - @Override - public void run() { - handleRpcError(t); - } - }); - } - - @Override - public void onCompleted() { - syncContext.execute(new Runnable() { - @Override - public void run() { - handleRpcCompleted(); - } - }); - } - }; - requestWriter = stub.withWaitForReady().streamAggregatedResources(responseReader); - } - - @Override - void sendDiscoveryRequest(DiscoveryRequestData request) { - checkState(requestWriter != null, "ADS stream has not been started"); - DiscoveryRequest requestProto = request.toEnvoyProto(); - requestWriter.onNext(requestProto); - logger.log(XdsLogLevel.DEBUG, "Sent DiscoveryRequest\n{0}", requestProto); - } - - @Override - void sendError(Exception error) { - requestWriter.onError(error); - } - } - - // TODO(chengyuanzhang): delete me. - private abstract class ResourceFetchTimeoutTask implements Runnable { - final String resourceName; - - ResourceFetchTimeoutTask(String resourceName) { - this.resourceName = resourceName; - } - - @Override - public void run() { - logger.log( - XdsLogLevel.WARNING, - "Did not receive resource info {0} after {1} seconds, conclude it absent", - resourceName, INITIAL_RESOURCE_FETCH_TIMEOUT_SEC); - } - } - - // TODO(chengyuanzhang): delete me. - @VisibleForTesting - final class LdsResourceFetchTimeoutTask extends ResourceFetchTimeoutTask { - - LdsResourceFetchTimeoutTask(String resourceName) { - super(resourceName); - } - - @Override - public void run() { - super.run(); - ldsRespTimer = null; - configWatcher.onResourceDoesNotExist(resourceName); - } - } - - @VisibleForTesting - final class ListenerResourceFetchTimeoutTask extends ResourceFetchTimeoutTask { - - ListenerResourceFetchTimeoutTask(String resourceName) { - super(resourceName); - } - - @Override - public void run() { - super.run(); - ldsRespTimer = null; - listenerWatcher.onResourceDoesNotExist(resourceName); - } - } - - // TODO(chengyuanzhang): delete me. - @VisibleForTesting - final class RdsResourceFetchTimeoutTask extends ResourceFetchTimeoutTask { - - RdsResourceFetchTimeoutTask(String resourceName) { - super(resourceName); - } - - @Override - public void run() { - super.run(); - rdsRespTimer = null; - configWatcher.onResourceDoesNotExist(resourceName); - } - } - - /** - * Returns {@code true} iff {@code hostName} matches the domain name {@code pattern} with - * case-insensitive. - * - *

Wildcard pattern rules: - *

    - *
  1. A single asterisk (*) matches any domain.
  2. - *
  3. Asterisk (*) is only permitted in the left-most or the right-most part of the pattern, - * but not both.
  4. - *
- */ - @VisibleForTesting - static boolean matchHostName(String hostName, String pattern) { - checkArgument(hostName.length() != 0 && !hostName.startsWith(".") && !hostName.endsWith("."), - "Invalid host name"); - checkArgument(pattern.length() != 0 && !pattern.startsWith(".") && !pattern.endsWith("."), - "Invalid pattern/domain name"); - - hostName = hostName.toLowerCase(Locale.US); - pattern = pattern.toLowerCase(Locale.US); - // hostName and pattern are now in lower case -- domain names are case-insensitive. - - if (!pattern.contains("*")) { - // Not a wildcard pattern -- hostName and pattern must match exactly. - return hostName.equals(pattern); - } - // Wildcard pattern - - if (pattern.length() == 1) { - return true; - } - - int index = pattern.indexOf('*'); - - // At most one asterisk (*) is allowed. - if (pattern.indexOf('*', index + 1) != -1) { - return false; - } - - // Asterisk can only match prefix or suffix. - if (index != 0 && index != pattern.length() - 1) { - return false; - } - - // HostName must be at least as long as the pattern because asterisk has to - // match one or more characters. - if (hostName.length() < pattern.length()) { - return false; - } - - if (index == 0 && hostName.endsWith(pattern.substring(1))) { - // Prefix matching fails. - return true; - } - - // Pattern matches hostname if suffix matching succeeds. - return index == pattern.length() - 1 - && hostName.startsWith(pattern.substring(0, pattern.length() - 1)); - } - - /** - * Convert protobuf message to human readable String format. Useful for protobuf messages - * containing {@link com.google.protobuf.Any} fields. - */ - @VisibleForTesting - static final class MessagePrinter { - private final JsonFormat.Printer printer; - - @VisibleForTesting - MessagePrinter() { - com.google.protobuf.TypeRegistry registry = - com.google.protobuf.TypeRegistry.newBuilder() - .add(Listener.getDescriptor()) - .add(io.envoyproxy.envoy.api.v2.Listener.getDescriptor()) - .add(HttpConnectionManager.getDescriptor()) - .add( - io.envoyproxy.envoy.config.filter.network.http_connection_manager.v2 - .HttpConnectionManager.getDescriptor()) - .add(RouteConfiguration.getDescriptor()) - .add(io.envoyproxy.envoy.api.v2.RouteConfiguration.getDescriptor()) - .add(Cluster.getDescriptor()) - .add(io.envoyproxy.envoy.api.v2.Cluster.getDescriptor()) - .add(ClusterLoadAssignment.getDescriptor()) - .add(io.envoyproxy.envoy.api.v2.ClusterLoadAssignment.getDescriptor()) - .build(); - printer = JsonFormat.printer().usingTypeRegistry(registry); - } - - @VisibleForTesting - String print(MessageOrBuilder message) { - String res; - try { - res = printer.print(message); - } catch (InvalidProtocolBufferException e) { - res = message + " (failed to pretty-print: " + e + ")"; - } - return res; - } - } - - @VisibleForTesting - static final class InvalidProtoDataException extends RuntimeException { - private static final long serialVersionUID = 1L; - - private InvalidProtoDataException(String message) { - super(message, null, false, false); - } - } -} diff --git a/xds/src/main/java/io/grpc/xds/XdsClientImpl2.java b/xds/src/main/java/io/grpc/xds/XdsClientImpl2.java index 02c943721b..826fd4a942 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClientImpl2.java +++ b/xds/src/main/java/io/grpc/xds/XdsClientImpl2.java @@ -983,7 +983,8 @@ final class XdsClientImpl2 extends XdsClient { } } - private String typeUrlV2() { + @VisibleForTesting + String typeUrlV2() { switch (this) { case LDS: return ADS_TYPE_URL_LDS_V2; diff --git a/xds/src/main/java/io/grpc/xds/XdsClientWrapperForServerSds.java b/xds/src/main/java/io/grpc/xds/XdsClientWrapperForServerSds.java index 08742d419b..6a815597b3 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClientWrapperForServerSds.java +++ b/xds/src/main/java/io/grpc/xds/XdsClientWrapperForServerSds.java @@ -57,7 +57,7 @@ import java.util.logging.Logger; import javax.annotation.Nullable; /** - * Serves as a wrapper for {@link XdsClientImpl} used on the server side by {@link + * Serves as a wrapper for {@link XdsClient} used on the server side by {@link * io.grpc.xds.internal.sds.XdsServerBuilder}. */ @Internal @@ -135,8 +135,8 @@ public final class XdsClientWrapperForServerSds { } Node node = bootstrapInfo.getNode(); timeService = SharedResourceHolder.get(timeServiceResource); - XdsClientImpl xdsClientImpl = - new XdsClientImpl( + XdsClientImpl2 xdsClientImpl = + new XdsClientImpl2( "", channel, node, diff --git a/xds/src/test/java/io/grpc/xds/XdsClientImplTest.java b/xds/src/test/java/io/grpc/xds/XdsClientImplTest.java deleted file mode 100644 index ae9e94a1e5..0000000000 --- a/xds/src/test/java/io/grpc/xds/XdsClientImplTest.java +++ /dev/null @@ -1,3739 +0,0 @@ -/* - * Copyright 2019 The gRPC Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.grpc.xds; - -import static com.google.common.truth.Truth.assertThat; -import static io.grpc.xds.XdsClientTestHelper.buildCluster; -import static io.grpc.xds.XdsClientTestHelper.buildClusterLoadAssignment; -import static io.grpc.xds.XdsClientTestHelper.buildDiscoveryRequest; -import static io.grpc.xds.XdsClientTestHelper.buildDiscoveryResponse; -import static io.grpc.xds.XdsClientTestHelper.buildDropOverload; -import static io.grpc.xds.XdsClientTestHelper.buildLbEndpoint; -import static io.grpc.xds.XdsClientTestHelper.buildListener; -import static io.grpc.xds.XdsClientTestHelper.buildLocalityLbEndpoints; -import static io.grpc.xds.XdsClientTestHelper.buildRouteConfiguration; -import static io.grpc.xds.XdsClientTestHelper.buildSecureCluster; -import static io.grpc.xds.XdsClientTestHelper.buildUpstreamTlsContext; -import static io.grpc.xds.XdsClientTestHelper.buildVirtualHost; -import static org.mockito.AdditionalAnswers.delegatesTo; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; -import com.google.common.util.concurrent.MoreExecutors; -import com.google.protobuf.Any; -import com.google.protobuf.BoolValue; -import com.google.protobuf.UInt32Value; -import com.google.protobuf.util.Durations; -import io.envoyproxy.envoy.config.core.v3.AggregatedConfigSource; -import io.envoyproxy.envoy.config.core.v3.ConfigSource; -import io.envoyproxy.envoy.config.core.v3.HealthStatus; -import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; -import io.envoyproxy.envoy.config.endpoint.v3.ClusterStats; -import io.envoyproxy.envoy.config.route.v3.QueryParameterMatcher; -import io.envoyproxy.envoy.config.route.v3.RedirectAction; -import io.envoyproxy.envoy.config.route.v3.Route; -import io.envoyproxy.envoy.config.route.v3.RouteAction; -import io.envoyproxy.envoy.config.route.v3.RouteConfiguration; -import io.envoyproxy.envoy.config.route.v3.RouteMatch; -import io.envoyproxy.envoy.config.route.v3.VirtualHost; -import io.envoyproxy.envoy.config.route.v3.WeightedCluster; -import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager; -import io.envoyproxy.envoy.extensions.filters.network.http_connection_manager.v3.Rds; -import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.SdsSecretConfig; -import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext; -import io.envoyproxy.envoy.service.discovery.v3.AggregatedDiscoveryServiceGrpc.AggregatedDiscoveryServiceImplBase; -import io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest; -import io.envoyproxy.envoy.service.discovery.v3.DiscoveryResponse; -import io.envoyproxy.envoy.service.load_stats.v3.LoadReportingServiceGrpc.LoadReportingServiceImplBase; -import io.envoyproxy.envoy.service.load_stats.v3.LoadStatsRequest; -import io.envoyproxy.envoy.service.load_stats.v3.LoadStatsResponse; -import io.grpc.Context; -import io.grpc.Context.CancellationListener; -import io.grpc.ManagedChannel; -import io.grpc.Status; -import io.grpc.Status.Code; -import io.grpc.SynchronizationContext; -import io.grpc.inprocess.InProcessChannelBuilder; -import io.grpc.inprocess.InProcessServerBuilder; -import io.grpc.internal.BackoffPolicy; -import io.grpc.internal.FakeClock; -import io.grpc.internal.FakeClock.ScheduledTask; -import io.grpc.internal.FakeClock.TaskFilter; -import io.grpc.stub.StreamObserver; -import io.grpc.testing.GrpcCleanupRule; -import io.grpc.xds.EnvoyProtoData.DropOverload; -import io.grpc.xds.EnvoyProtoData.LbEndpoint; -import io.grpc.xds.EnvoyProtoData.Locality; -import io.grpc.xds.EnvoyProtoData.LocalityLbEndpoints; -import io.grpc.xds.EnvoyProtoData.Node; -import io.grpc.xds.XdsClient.CdsResourceWatcher; -import io.grpc.xds.XdsClient.CdsUpdate; -import io.grpc.xds.XdsClient.ConfigUpdate; -import io.grpc.xds.XdsClient.ConfigWatcher; -import io.grpc.xds.XdsClient.EdsResourceWatcher; -import io.grpc.xds.XdsClient.EdsUpdate; -import io.grpc.xds.XdsClient.XdsChannel; -import io.grpc.xds.XdsClientImpl.MessagePrinter; -import io.grpc.xds.XdsClientImpl.ResourceType; -import java.io.IOException; -import java.util.ArrayDeque; -import java.util.HashSet; -import java.util.List; -import java.util.Queue; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatcher; -import org.mockito.InOrder; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -/** - * Tests for {@link XdsClientImpl} with xDS v3 protocol. However, the test xDS server still sends - * update with v2 resources for testing compatibility. - */ -@RunWith(JUnit4.class) -public class XdsClientImplTest { - - private static final String TARGET_AUTHORITY = "foo.googleapis.com:8080"; - - private static final Node NODE = Node.newBuilder().build(); - private static final FakeClock.TaskFilter RPC_RETRY_TASK_FILTER = - new FakeClock.TaskFilter() { - @Override - public boolean shouldAccept(Runnable command) { - return command.toString().contains(XdsClientImpl.RpcRetryTask.class.getSimpleName()); - } - }; - - private static final FakeClock.TaskFilter LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER = - new TaskFilter() { - @Override - public boolean shouldAccept(Runnable command) { - return command.toString() - .contains(XdsClientImpl.LdsResourceFetchTimeoutTask.class.getSimpleName()); - } - }; - - private static final FakeClock.TaskFilter RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER = - new TaskFilter() { - @Override - public boolean shouldAccept(Runnable command) { - return command.toString() - .contains(XdsClientImpl.RdsResourceFetchTimeoutTask.class.getSimpleName()); - } - }; - - private static final FakeClock.TaskFilter CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER = - new TaskFilter() { - @Override - public boolean shouldAccept(Runnable command) { - return command.toString().contains(ResourceType.CDS.toString()); - } - }; - - private static final FakeClock.TaskFilter EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER = - new FakeClock.TaskFilter() { - @Override - public boolean shouldAccept(Runnable command) { - return command.toString().contains(ResourceType.EDS.toString()); - } - }; - - @Rule - public final GrpcCleanupRule cleanupRule = new GrpcCleanupRule(); - @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 - @Rule - public ExpectedException thrown = ExpectedException.none(); - - private final SynchronizationContext syncContext = new SynchronizationContext( - new Thread.UncaughtExceptionHandler() { - @Override - public void uncaughtException(Thread t, Throwable e) { - throw new AssertionError(e); - } - }); - private final FakeClock fakeClock = new FakeClock(); - - private final Queue> responseObservers = new ArrayDeque<>(); - private final Queue> requestObservers = new ArrayDeque<>(); - private final AtomicBoolean adsEnded = new AtomicBoolean(true); - private final Queue loadReportCalls = new ArrayDeque<>(); - private final AtomicBoolean lrsEnded = new AtomicBoolean(true); - - @Mock - private AggregatedDiscoveryServiceImplBase mockedDiscoveryService; - @Mock - private BackoffPolicy.Provider backoffPolicyProvider; - @Mock - private BackoffPolicy backoffPolicy1; - @Mock - private BackoffPolicy backoffPolicy2; - @Mock - private ConfigWatcher configWatcher; - @Mock - private CdsResourceWatcher cdsResourceWatcher; - @Mock - private EdsResourceWatcher edsResourceWatcher; - - private ManagedChannel channel; - private XdsClientImpl xdsClient; - - @Before - public void setUp() throws IOException { - MockitoAnnotations.initMocks(this); - when(backoffPolicyProvider.get()).thenReturn(backoffPolicy1, backoffPolicy2); - when(backoffPolicy1.nextBackoffNanos()).thenReturn(10L, 100L); - when(backoffPolicy2.nextBackoffNanos()).thenReturn(20L, 200L); - - final String serverName = InProcessServerBuilder.generateName(); - AggregatedDiscoveryServiceImplBase adsServiceImpl = new AggregatedDiscoveryServiceImplBase() { - @Override - public StreamObserver streamAggregatedResources( - final StreamObserver responseObserver) { - assertThat(adsEnded.get()).isTrue(); // ensure previous call was ended - adsEnded.set(false); - Context.current().addListener( - new CancellationListener() { - @Override - public void cancelled(Context context) { - adsEnded.set(true); - } - }, MoreExecutors.directExecutor()); - responseObservers.offer(responseObserver); - @SuppressWarnings("unchecked") - StreamObserver requestObserver = mock(StreamObserver.class); - requestObservers.offer(requestObserver); - return requestObserver; - } - }; - mockedDiscoveryService = - mock(AggregatedDiscoveryServiceImplBase.class, delegatesTo(adsServiceImpl)); - - LoadReportingServiceImplBase lrsServiceImpl = new LoadReportingServiceImplBase() { - @Override - public StreamObserver streamLoadStats( - StreamObserver responseObserver) { - assertThat(lrsEnded.get()).isTrue(); - lrsEnded.set(false); - @SuppressWarnings("unchecked") - StreamObserver requestObserver = mock(StreamObserver.class); - final LoadReportCall call = new LoadReportCall(requestObserver, responseObserver); - Context.current().addListener( - new CancellationListener() { - @Override - public void cancelled(Context context) { - lrsEnded.set(true); - } - }, MoreExecutors.directExecutor()); - loadReportCalls.offer(call); - return requestObserver; - } - }; - - cleanupRule.register( - InProcessServerBuilder - .forName(serverName) - .addService(mockedDiscoveryService) - .addService(lrsServiceImpl) - .directExecutor() - .build() - .start()); - channel = - cleanupRule.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()); - - xdsClient = - new XdsClientImpl( - TARGET_AUTHORITY, - new XdsChannel(channel, /* useProtocolV3= */ true), - EnvoyProtoData.Node.newBuilder().build(), - syncContext, - fakeClock.getScheduledExecutorService(), - backoffPolicyProvider, - fakeClock.getStopwatchSupplier()); - // Only the connection to management server is established, no RPC request is sent until at - // least one watcher is registered. - assertThat(responseObservers).isEmpty(); - assertThat(requestObservers).isEmpty(); - } - - @After - public void tearDown() { - xdsClient.shutdown(); - assertThat(adsEnded.get()).isTrue(); - assertThat(lrsEnded.get()).isTrue(); - assertThat(channel.isShutdown()).isTrue(); - assertThat(fakeClock.getPendingTasks()).isEmpty(); - } - - // Always test the real workflow and integrity of XdsClient: RDS protocol should always followed - // after at least one LDS request-response, from which the RDS resource name comes. CDS and EDS - // can be tested separately as they are used in a standalone way. - - // Discovery responses should follow management server spec and xDS protocol. See - // https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol. - - /** - * Client receives an LDS response that does not contain a Listener for the requested resource. - * The LDS response is ACKed. - * The config watcher is notified with resource unavailable after its response timer expires. - */ - @Test - public void ldsResponseWithoutMatchingResource() { - xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends an LDS request for the host name (with port) to management server. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - - assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - - List listeners = ImmutableList.of( - Any.pack(buildListener("bar.googleapis.com", - Any.pack(HttpConnectionManager.newBuilder() - .setRouteConfig( - buildRouteConfiguration("route-bar.googleapis.com", - ImmutableList.of( - buildVirtualHost( - ImmutableList.of("bar.googleapis.com"), - "cluster-bar.googleapis.com")))) - .build()))), - Any.pack(buildListener("baz.googleapis.com", - Any.pack(HttpConnectionManager.newBuilder() - .setRouteConfig( - buildRouteConfiguration("route-baz.googleapis.com", - ImmutableList.of( - buildVirtualHost( - ImmutableList.of("baz.googleapis.com"), - "cluster-baz.googleapis.com")))) - .build())))); - DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000"); - responseObserver.onNext(response); - - // Client sends an ACK LDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, "0000"))); - - verify(configWatcher, never()).onConfigChanged(any(ConfigUpdate.class)); - verify(configWatcher, never()).onResourceDoesNotExist(TARGET_AUTHORITY); - verify(configWatcher, never()).onError(any(Status.class)); - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(configWatcher).onResourceDoesNotExist(TARGET_AUTHORITY); - assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); - } - - /** - * An LDS response contains the requested listener and an in-lined RouteConfiguration message for - * that listener. But the RouteConfiguration message is invalid as it does not contain any - * VirtualHost with domains matching the requested hostname. - * The LDS response is NACKed, as if the XdsClient has not received this response. - * The config watcher is notified with an error after its response timer expires.. - */ - @Test - public void failToFindVirtualHostInLdsResponseInLineRouteConfig() { - xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends an LDS request for the host name (with port) to management server. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - - io.envoyproxy.envoy.config.route.v3.RouteConfiguration routeConfig = - buildRouteConfiguration( - "route.googleapis.com", - ImmutableList.of( - buildVirtualHost(ImmutableList.of("something does not match"), - "some cluster"), - buildVirtualHost(ImmutableList.of("something else does not match"), - "some other cluster"))); - - List listeners = ImmutableList.of( - Any.pack(buildListener(TARGET_AUTHORITY, /* matching resource */ - Any.pack(HttpConnectionManager.newBuilder().setRouteConfig(routeConfig).build())))); - DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000"); - responseObserver.onNext(response); - - // Client sends an NACK LDS request. - verify(requestObserver) - .onNext( - argThat(new DiscoveryRequestMatcher("", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, "0000"))); - - verify(configWatcher, never()).onConfigChanged(any(ConfigUpdate.class)); - verify(configWatcher, never()).onResourceDoesNotExist(TARGET_AUTHORITY); - verify(configWatcher, never()).onError(any(Status.class)); - - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(configWatcher).onResourceDoesNotExist(TARGET_AUTHORITY); - assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); - } - - /** - * Client resolves the virtual host config from an LDS response that contains a - * RouteConfiguration message directly in-line for the requested resource. No RDS is needed. - * The LDS response is ACKed. - * The config watcher is notified with an update. - */ - @Test - public void resolveVirtualHostInLdsResponse() { - xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends an LDS request for the host name (with port) to management server. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - ScheduledTask ldsRespTimer = - Iterables.getOnlyElement( - fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); - assertThat(ldsRespTimer.isCancelled()).isFalse(); - - List listeners = ImmutableList.of( - Any.pack(buildListener("bar.googleapis.com", - Any.pack(HttpConnectionManager.newBuilder() - .setRouteConfig( - buildRouteConfiguration("route-bar.googleapis.com", - ImmutableList.of( - buildVirtualHost( - ImmutableList.of("bar.googleapis.com"), - "cluster-bar.googleapis.com")))) - .build()))), - Any.pack(buildListener("baz.googleapis.com", - Any.pack(HttpConnectionManager.newBuilder() - .setRouteConfig( - buildRouteConfiguration("route-baz.googleapis.com", - ImmutableList.of( - buildVirtualHost( - ImmutableList.of("baz.googleapis.com"), - "cluster-baz.googleapis.com")))) - .build()))), - Any.pack(buildListener(TARGET_AUTHORITY, /* matching resource */ - Any.pack( - HttpConnectionManager.newBuilder() - .setRouteConfig( // target route configuration - buildRouteConfiguration("route-foo.googleapis.com", - ImmutableList.of( - buildVirtualHost( // matching virtual host - ImmutableList.of(TARGET_AUTHORITY, "bar.googleapis.com"), - "cluster.googleapis.com"), - buildVirtualHost( - ImmutableList.of("something does not match"), - "some cluster")))) - .build())))); - DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000"); - responseObserver.onNext(response); - - assertThat(ldsRespTimer.isCancelled()).isTrue(); - - // Client sends an ACK request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, "0000"))); - - ArgumentCaptor configUpdateCaptor = ArgumentCaptor.forClass(null); - verify(configWatcher).onConfigChanged(configUpdateCaptor.capture()); - assertConfigUpdateContainsSingleClusterRoute( - configUpdateCaptor.getValue(), "cluster.googleapis.com"); - - verifyNoMoreInteractions(requestObserver); - } - - /** - * Client receives an RDS response (after a previous LDS request-response) that does not contain a - * RouteConfiguration for the requested resource while each received RouteConfiguration is valid. - * The RDS response is ACKed. - * After the resource fetch timeout expires, watcher waiting for the resource is notified - * with resource unavailable. - */ - @Test - public void rdsResponseWithoutMatchingResource() { - xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends an LDS request for the host name (with port) to management server. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - - Rds rdsConfig = - Rds.newBuilder() - // Must set to use ADS. - .setConfigSource( - ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) - .setRouteConfigName("route-foo.googleapis.com") - .build(); - List listeners = ImmutableList.of( - Any.pack(buildListener(TARGET_AUTHORITY, /* matching resource */ - Any.pack(HttpConnectionManager.newBuilder().setRds(rdsConfig).build()))) - ); - DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000"); - responseObserver.onNext(response); - - // Client sends an ACK LDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, "0000"))); - - // Client sends an (first) RDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS, ""))); - - assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - - // Management server should only sends RouteConfiguration messages with at least one - // VirtualHost with domains matching requested hostname. Otherwise, it is invalid data. - List routeConfigs = ImmutableList.of( - Any.pack( - buildRouteConfiguration( - "some resource name does not match route-foo.googleapis.com", - ImmutableList.of( - buildVirtualHost( - ImmutableList.of(TARGET_AUTHORITY), - "whatever cluster")))), - Any.pack( - buildRouteConfiguration( - "some other resource name does not match route-foo.googleapis.com", - ImmutableList.of( - buildVirtualHost( - ImmutableList.of(TARGET_AUTHORITY), - "some more whatever cluster"))))); - response = buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0000"); - responseObserver.onNext(response); - - // Client sends an ACK RDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS, "0000"))); - - verify(configWatcher, never()).onConfigChanged(any(ConfigUpdate.class)); - verify(configWatcher, never()).onResourceDoesNotExist(anyString()); - verify(configWatcher, never()).onError(any(Status.class)); - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(configWatcher).onResourceDoesNotExist("route-foo.googleapis.com"); - assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); - } - - /** - * Client resolves the virtual host config from an RDS response for the requested resource. The - * RDS response is ACKed. - * The config watcher is notified with an update. - */ - @Test - public void resolveVirtualHostInRdsResponse() { - xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - Rds rdsConfig = - Rds.newBuilder() - // Must set to use ADS. - .setConfigSource( - ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) - .setRouteConfigName("route-foo.googleapis.com") - .build(); - - List listeners = ImmutableList.of( - Any.pack(buildListener(TARGET_AUTHORITY, /* matching resource */ - Any.pack(HttpConnectionManager.newBuilder().setRds(rdsConfig).build()))) - ); - DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000"); - responseObserver.onNext(response); - - // Client sends an ACK LDS request and an RDS request for "route-foo.googleapis.com". (Omitted) - - assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - - // Management server should only sends RouteConfiguration messages with at least one - // VirtualHost with domains matching requested hostname. Otherwise, it is invalid data. - List routeConfigs = ImmutableList.of( - Any.pack( - buildRouteConfiguration( - "route-foo.googleapis.com", // target route configuration - ImmutableList.of( - buildVirtualHost(ImmutableList.of("something does not match"), - "some cluster"), - buildVirtualHost(ImmutableList.of(TARGET_AUTHORITY, "bar.googleapis.com:443"), - "cluster.googleapis.com")))), // matching virtual host - Any.pack( - buildRouteConfiguration( - "some resource name does not match route-foo.googleapis.com", - ImmutableList.of( - buildVirtualHost(ImmutableList.of("foo.googleapis.com"), - "some more cluster"))))); - response = buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0000"); - responseObserver.onNext(response); - - assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); - - // Client sent an ACK RDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS, "0000"))); - - ArgumentCaptor configUpdateCaptor = ArgumentCaptor.forClass(null); - verify(configWatcher).onConfigChanged(configUpdateCaptor.capture()); - assertConfigUpdateContainsSingleClusterRoute( - configUpdateCaptor.getValue(), "cluster.googleapis.com"); - } - - /** - * Client resolves the virtual host config with path matching from an RDS response for the - * requested resource. The RDS response is ACKed. - * The config watcher is notified with an update. - */ - @Test - public void resolveVirtualHostWithPathMatchingInRdsResponse() { - xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - Rds rdsConfig = - Rds.newBuilder() - // Must set to use ADS. - .setConfigSource( - ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) - .setRouteConfigName("route-foo.googleapis.com") - .build(); - - List listeners = ImmutableList.of( - Any.pack(buildListener(TARGET_AUTHORITY, /* matching resource */ - Any.pack(HttpConnectionManager.newBuilder().setRds(rdsConfig).build()))) - ); - DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000"); - responseObserver.onNext(response); - - // Client sends an ACK LDS request and an RDS request for "route-foo.googleapis.com". (Omitted) - - assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - - // Management server should only sends RouteConfiguration messages with at least one - // VirtualHost with domains matching requested hostname. Otherwise, it is invalid data. - List routeConfigs = - ImmutableList.of( - Any.pack( - buildRouteConfiguration( - "route-foo.googleapis.com", - ImmutableList.of( - VirtualHost.newBuilder() - .setName("virtualhost00.googleapis.com") // don't care - // domains wit a match. - .addAllDomains(ImmutableList.of(TARGET_AUTHORITY, "bar.googleapis.com")) - .addRoutes( - Route.newBuilder() - // path match with cluster route - .setRoute( - RouteAction.newBuilder() - .setCluster("cl1.googleapis.com")) - .setMatch( - RouteMatch.newBuilder() - .setPath("/service1/method1"))) - .addRoutes( - Route.newBuilder() - // path match with weighted cluster route - .setRoute( - RouteAction.newBuilder() - .setWeightedClusters( - WeightedCluster.newBuilder() - .addClusters( - WeightedCluster.ClusterWeight.newBuilder() - .setWeight(UInt32Value.of(30)) - .setName("cl21.googleapis.com")) - .addClusters( - WeightedCluster.ClusterWeight.newBuilder() - .setWeight(UInt32Value.of(70)) - .setName("cl22.googleapis.com")))) - .setMatch( - RouteMatch.newBuilder() - .setPath("/service2/method2"))) - .addRoutes( - Route.newBuilder() - // prefix match with cluster route - .setRoute( - RouteAction.newBuilder() - .setCluster("cl1.googleapis.com")) - .setMatch( - RouteMatch.newBuilder() - .setPrefix("/service1/"))) - .addRoutes( - Route.newBuilder() - // default match with cluster route - .setRoute( - RouteAction.newBuilder() - .setCluster("cluster.googleapis.com")) - .setMatch( - RouteMatch.newBuilder() - .setPrefix(""))) - .build())))); - response = buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0000"); - responseObserver.onNext(response); - - assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); - - // Client sent an ACK RDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS, "0000"))); - - ArgumentCaptor configUpdateCaptor = ArgumentCaptor.forClass(null); - verify(configWatcher).onConfigChanged(configUpdateCaptor.capture()); - List routes = configUpdateCaptor.getValue().getRoutes(); - assertThat(routes).hasSize(4); - assertThat(routes.get(0)) - .isEqualTo( - new EnvoyProtoData.Route( - // path match with cluster route - new io.grpc.xds.RouteMatch( - /* pathPrefixMatch= */ null, /* pathExactMatch= */ "/service1/method1"), - new EnvoyProtoData.RouteAction( - TimeUnit.SECONDS.toNanos(15L), "cl1.googleapis.com", null))); - assertThat(routes.get(1)) - .isEqualTo( - new EnvoyProtoData.Route( - // path match with weighted cluster route - new io.grpc.xds.RouteMatch( - /* pathPrefixMatch= */ null, /* pathExactMatch= */ "/service2/method2"), - new EnvoyProtoData.RouteAction( - TimeUnit.SECONDS.toNanos(15L), - null, - ImmutableList.of( - new EnvoyProtoData.ClusterWeight("cl21.googleapis.com", 30), - new EnvoyProtoData.ClusterWeight("cl22.googleapis.com", 70))))); - assertThat(routes.get(2)) - .isEqualTo( - new EnvoyProtoData.Route( - // prefix match with cluster route - new io.grpc.xds.RouteMatch( - /* pathPrefixMatch= */ "/service1/", /* pathExactMatch= */ null), - new EnvoyProtoData.RouteAction( - TimeUnit.SECONDS.toNanos(15L), "cl1.googleapis.com", null))); - assertThat(routes.get(3)) - .isEqualTo( - new EnvoyProtoData.Route( - // default match with cluster route - new io.grpc.xds.RouteMatch( - /* pathPrefixMatch= */ "", /* pathExactMatch= */ null), - new EnvoyProtoData.RouteAction( - TimeUnit.SECONDS.toNanos(15L), "cluster.googleapis.com", null))); - } - - /** - * Client receives an RDS response (after a previous LDS request-response) containing a - * RouteConfiguration message for the requested resource. But the RouteConfiguration message - * is invalid as it does not contain any VirtualHost with domains matching the requested - * hostname. - * The RDS response is NACKed, as if the XdsClient has not received this response. - */ - @Test - public void failToFindVirtualHostInRdsResponse() { - xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - Rds rdsConfig = - Rds.newBuilder() - // Must set to use ADS. - .setConfigSource( - ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) - .setRouteConfigName("route-foo.googleapis.com") - .build(); - - List listeners = ImmutableList.of( - Any.pack(buildListener(TARGET_AUTHORITY, /* matching resource */ - Any.pack(HttpConnectionManager.newBuilder().setRds(rdsConfig).build()))) - ); - DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000"); - responseObserver.onNext(response); - - // Client sends an ACK LDS request and an RDS request for "route-foo.googleapis.com". (Omitted) - - assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - - List routeConfigs = ImmutableList.of( - Any.pack( - buildRouteConfiguration( - "route-foo.googleapis.com", - ImmutableList.of( - buildVirtualHost(ImmutableList.of("something does not match"), - "some cluster"), - buildVirtualHost( - ImmutableList.of("something else does not match", "also does not match"), - "cluster.googleapis.com")))), - Any.pack( - buildRouteConfiguration( - "some resource name does not match route-foo.googleapis.com", - ImmutableList.of( - buildVirtualHost(ImmutableList.of("one more does not match"), - "some more cluster"))))); - response = buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0000"); - responseObserver.onNext(response); - - // Client sent an NACK RDS request. - verify(requestObserver) - .onNext( - argThat(new DiscoveryRequestMatcher("", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS, "0000"))); - - verify(configWatcher, never()).onConfigChanged(any(ConfigUpdate.class)); - verify(configWatcher, never()).onResourceDoesNotExist(anyString()); - verify(configWatcher, never()).onError(any(Status.class)); - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(configWatcher).onResourceDoesNotExist("route-foo.googleapis.com"); - assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); - } - - /** - * Client receives an RDS response (after a previous LDS request-response) containing a - * RouteConfiguration message for the requested resource. But the RouteConfiguration message - * is invalid as the VirtualHost with domains matching the requested hostname contains invalid - * data, its RouteAction message is absent. - * The RDS response is NACKed, as if the XdsClient has not received this response. - */ - @Test - public void matchingVirtualHostDoesNotContainRouteAction() { - xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - Rds rdsConfig = - Rds.newBuilder() - // Must set to use ADS. - .setConfigSource( - ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) - .setRouteConfigName("route-foo.googleapis.com") - .build(); - - List listeners = ImmutableList.of( - Any.pack(buildListener(TARGET_AUTHORITY, /* matching resource */ - Any.pack(HttpConnectionManager.newBuilder().setRds(rdsConfig).build()))) - ); - DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000"); - responseObserver.onNext(response); - - // Client sends an ACK LDS request and an RDS request for "route-foo.googleapis.com". (Omitted) - - assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - - // A VirtualHost with a Route that contains only redirect configuration. - VirtualHost virtualHost = - VirtualHost.newBuilder() - .setName("virtualhost00.googleapis.com") // don't care - .addDomains(TARGET_AUTHORITY) - .addRoutes( - Route.newBuilder() - .setRedirect( - RedirectAction.newBuilder() - .setHostRedirect("bar.googleapis.com") - .setPortRedirect(443))) - .build(); - - List routeConfigs = ImmutableList.of( - Any.pack( - buildRouteConfiguration("route-foo.googleapis.com", - ImmutableList.of(virtualHost)))); - response = buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0000"); - responseObserver.onNext(response); - - // Client sent an NACK RDS request. - verify(requestObserver) - .onNext( - argThat(new DiscoveryRequestMatcher("", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS, "0000"))); - - verify(configWatcher, never()).onConfigChanged(any(ConfigUpdate.class)); - verify(configWatcher, never()).onResourceDoesNotExist(anyString()); - verify(configWatcher, never()).onError(any(Status.class)); - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(configWatcher).onResourceDoesNotExist("route-foo.googleapis.com"); - assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); - } - - /** - * Client receives LDS/RDS responses for updating resources previously received. - * - *

Tests for streaming behavior. - */ - @Test - public void notifyUpdatedResources() { - xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends an LDS request for the host name (with port) to management server. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - - // Management server sends back an LDS response containing a RouteConfiguration for the - // requested Listener directly in-line. - RouteConfiguration routeConfig = - buildRouteConfiguration( - "route-foo.googleapis.com", // target route configuration - ImmutableList.of( - buildVirtualHost( // matching virtual host - ImmutableList.of(TARGET_AUTHORITY, "bar.googleapis.com:443"), - "cluster.googleapis.com"), - buildVirtualHost(ImmutableList.of("something does not match"), - "some cluster"))); - - List listeners = ImmutableList.of( - Any.pack(buildListener(TARGET_AUTHORITY, /* matching resource */ - Any.pack(HttpConnectionManager.newBuilder().setRouteConfig(routeConfig).build()))) - ); - DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000"); - responseObserver.onNext(response); - - // Client sends an ACK LDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, "0000"))); - - // Cluster name is resolved and notified to config watcher. - ArgumentCaptor configUpdateCaptor = ArgumentCaptor.forClass(null); - verify(configWatcher).onConfigChanged(configUpdateCaptor.capture()); - assertConfigUpdateContainsSingleClusterRoute( - configUpdateCaptor.getValue(), "cluster.googleapis.com"); - - // Management sends back another LDS response containing updates for the requested Listener. - routeConfig = - buildRouteConfiguration( - "another-route-foo.googleapis.com", - ImmutableList.of( - buildVirtualHost(ImmutableList.of(TARGET_AUTHORITY, "bar.googleapis.com:443"), - "another-cluster.googleapis.com"), - buildVirtualHost(ImmutableList.of("something does not match"), - "some cluster"))); - - listeners = ImmutableList.of( - Any.pack(buildListener(TARGET_AUTHORITY, /* matching resource */ - Any.pack(HttpConnectionManager.newBuilder().setRouteConfig(routeConfig).build()))) - ); - response = - buildDiscoveryResponse("1", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0001"); - responseObserver.onNext(response); - - // Client sends an ACK LDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "1", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, "0001"))); - - // Updated cluster name is notified to config watcher. - configUpdateCaptor = ArgumentCaptor.forClass(null); - verify(configWatcher, times(2)).onConfigChanged(configUpdateCaptor.capture()); - assertConfigUpdateContainsSingleClusterRoute( - configUpdateCaptor.getValue(), "another-cluster.googleapis.com"); - - // Management server sends back another LDS response containing updates for the requested - // Listener and telling client to do RDS. - Rds rdsConfig = - Rds.newBuilder() - // Must set to use ADS. - .setConfigSource( - ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) - .setRouteConfigName("some-route-to-foo.googleapis.com") - .build(); - - listeners = ImmutableList.of( - Any.pack(buildListener(TARGET_AUTHORITY, /* matching resource */ - Any.pack(HttpConnectionManager.newBuilder().setRds(rdsConfig).build()))) - ); - response = - buildDiscoveryResponse("2", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0002"); - responseObserver.onNext(response); - - // Client sends an ACK LDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "2", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, "0002"))); - - // Client sends an (first) RDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "some-route-to-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS, ""))); - - // Management server sends back an RDS response containing the RouteConfiguration - // for the requested resource. - List routeConfigs = ImmutableList.of( - Any.pack( - buildRouteConfiguration( - "some-route-to-foo.googleapis.com", - ImmutableList.of( - buildVirtualHost(ImmutableList.of("something does not match"), - "some cluster"), - buildVirtualHost(ImmutableList.of(TARGET_AUTHORITY, "bar.googleapis.com:443"), - "some-other-cluster.googleapis.com"))))); - response = buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0000"); - responseObserver.onNext(response); - - // Client sent an ACK RDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "some-route-to-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS, "0000"))); - - // Updated cluster name is notified to config watcher again. - configUpdateCaptor = ArgumentCaptor.forClass(null); - verify(configWatcher, times(3)).onConfigChanged(configUpdateCaptor.capture()); - assertConfigUpdateContainsSingleClusterRoute( - configUpdateCaptor.getValue(), "some-other-cluster.googleapis.com"); - - // Management server sends back another RDS response containing updated information for the - // RouteConfiguration currently in-use by client. - routeConfigs = ImmutableList.of( - Any.pack( - buildRouteConfiguration( - "some-route-to-foo.googleapis.com", - ImmutableList.of( - buildVirtualHost(ImmutableList.of(TARGET_AUTHORITY, "bar.googleapis.com:443"), - "an-updated-cluster.googleapis.com"))))); - response = buildDiscoveryResponse("1", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0001"); - responseObserver.onNext(response); - - // Client sent an ACK RDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "1", "some-route-to-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS, "0001"))); - - // Updated cluster name is notified to config watcher again. - configUpdateCaptor = ArgumentCaptor.forClass(null); - verify(configWatcher, times(4)).onConfigChanged(configUpdateCaptor.capture()); - assertConfigUpdateContainsSingleClusterRoute( - configUpdateCaptor.getValue(), "an-updated-cluster.googleapis.com"); - - // Management server sends back an LDS response indicating all Listener resources are removed. - response = - buildDiscoveryResponse("3", ImmutableList.of(), - XdsClientImpl.ADS_TYPE_URL_LDS, "0003"); - responseObserver.onNext(response); - - verify(configWatcher).onResourceDoesNotExist(TARGET_AUTHORITY); - } - - // TODO(chengyuanzhang): tests for timeout waiting for responses for incremental - // protocols (RDS/EDS). - - /** - * Client receives multiple RDS responses without RouteConfiguration for the requested - * resource. It should continue waiting until such an RDS response arrives, as RDS - * protocol is incremental. - * - *

Tests for RDS incremental protocol behavior. - */ - @Test - public void waitRdsResponsesForRequestedResource() { - xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends an LDS request for the host name (with port) to management server. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - - // Management sends back an LDS response telling client to do RDS. - Rds rdsConfig = - Rds.newBuilder() - // Must set to use ADS. - .setConfigSource( - ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) - .setRouteConfigName("route-foo.googleapis.com") - .build(); - - List listeners = ImmutableList.of( - Any.pack(buildListener(TARGET_AUTHORITY, /* matching resource */ - Any.pack(HttpConnectionManager.newBuilder().setRds(rdsConfig).build()))) - ); - DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000"); - responseObserver.onNext(response); - - // Client sends an ACK LDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, "0000"))); - - // Client sends an (first) RDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS, ""))); - - ScheduledTask rdsRespTimer = - Iterables.getOnlyElement( - fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); - assertThat(rdsRespTimer.isCancelled()).isFalse(); - - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC - 2, TimeUnit.SECONDS); - - // Management server sends back an RDS response that does not contain RouteConfiguration - // for the requested resource. - List routeConfigs = ImmutableList.of( - Any.pack( - buildRouteConfiguration( - "some resource name does not match route-foo.googleapis.com", - ImmutableList.of( - buildVirtualHost( - ImmutableList.of(TARGET_AUTHORITY), - "some more cluster"))))); - response = buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0000"); - responseObserver.onNext(response); - - // Client sent an ACK RDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS, "0000"))); - - // Client waits for future RDS responses silently. - verifyNoMoreInteractions(configWatcher); - assertThat(rdsRespTimer.isCancelled()).isFalse(); - - fakeClock.forwardTime(1, TimeUnit.SECONDS); - - // Management server sends back another RDS response containing the RouteConfiguration - // for the requested resource. - routeConfigs = ImmutableList.of( - Any.pack( - buildRouteConfiguration( - "route-foo.googleapis.com", // target route configuration - ImmutableList.of( - buildVirtualHost( - ImmutableList.of("something does not match"), - "some cluster"), - buildVirtualHost( // matching virtual host - ImmutableList.of(TARGET_AUTHORITY, "bar.googleapis.com:443"), - "another-cluster.googleapis.com"))))); - response = buildDiscoveryResponse("1", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0001"); - responseObserver.onNext(response); - - // Client sent an ACK RDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "1", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS, "0001"))); - - // Updated cluster name is notified to config watcher. - ArgumentCaptor configUpdateCaptor = ArgumentCaptor.forClass(null); - verify(configWatcher).onConfigChanged(configUpdateCaptor.capture()); - assertConfigUpdateContainsSingleClusterRoute( - configUpdateCaptor.getValue(), "another-cluster.googleapis.com"); - assertThat(rdsRespTimer.isCancelled()).isTrue(); - } - - /** - * An RouteConfiguration is removed by server by sending client an LDS response removing the - * corresponding Listener. - */ - @Test - public void routeConfigurationRemovedNotifiedToWatcher() { - xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends an LDS request for the host name (with port) to management server. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - - // Management sends back an LDS response telling client to do RDS. - Rds rdsConfig = - Rds.newBuilder() - // Must set to use ADS. - .setConfigSource( - ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) - .setRouteConfigName("route-foo.googleapis.com") - .build(); - - List listeners = ImmutableList.of( - Any.pack(buildListener(TARGET_AUTHORITY, /* matching resource */ - Any.pack(HttpConnectionManager.newBuilder().setRds(rdsConfig).build()))) - ); - DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000"); - responseObserver.onNext(response); - - // Client sends an ACK LDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, "0000"))); - - // Client sends an (first) RDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS, ""))); - - // Management server sends back an RDS response containing RouteConfiguration requested. - List routeConfigs = ImmutableList.of( - Any.pack( - buildRouteConfiguration( - "route-foo.googleapis.com", // target route configuration - ImmutableList.of( - buildVirtualHost( - ImmutableList.of(TARGET_AUTHORITY), // matching virtual host - "cluster.googleapis.com"))))); - response = buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0000"); - responseObserver.onNext(response); - - // Client sent an ACK RDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS, "0000"))); - - // Resolved cluster name is notified to config watcher. - ArgumentCaptor configUpdateCaptor = ArgumentCaptor.forClass(null); - verify(configWatcher).onConfigChanged(configUpdateCaptor.capture()); - assertConfigUpdateContainsSingleClusterRoute( - configUpdateCaptor.getValue(), "cluster.googleapis.com"); - - // Management server sends back another LDS response with the previous Listener (currently - // in-use by client) removed as the RouteConfiguration it references to is absent. - response = - buildDiscoveryResponse("1", ImmutableList.of(), // empty - XdsClientImpl.ADS_TYPE_URL_LDS, "0001"); - responseObserver.onNext(response); - - // Client sent an ACK LDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "1", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, "0001"))); - - verify(configWatcher).onResourceDoesNotExist(TARGET_AUTHORITY); - } - - /** - * Management server sends another LDS response for updating the RDS resource to be requested - * while client is currently requesting for a previously given RDS resource name. - */ - @Test - public void updateRdsRequestResourceWhileInitialResourceFetchInProgress() { - xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Management sends back an LDS response telling client to do RDS. - Rds rdsConfig = - Rds.newBuilder() - // Must set to use ADS. - .setConfigSource( - ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) - .setRouteConfigName("route-foo.googleapis.com") - .build(); - - List listeners = ImmutableList.of( - Any.pack(buildListener(TARGET_AUTHORITY, /* matching resource */ - Any.pack(HttpConnectionManager.newBuilder().setRds(rdsConfig).build()))) - ); - DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000"); - responseObserver.onNext(response); - - // Client sends an (first) RDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS, ""))); - - ScheduledTask rdsRespTimer = - Iterables.getOnlyElement( - fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); - assertThat(rdsRespTimer.isCancelled()).isFalse(); - - // Management sends back another LDS response updating the Listener information to use - // another resource name for doing RDS. - rdsConfig = - Rds.newBuilder() - // Must set to use ADS. - .setConfigSource( - ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) - .setRouteConfigName("route-bar.googleapis.com") - .build(); - - listeners = ImmutableList.of( - Any.pack( - buildListener( - TARGET_AUTHORITY, /* matching resource */ - Any.pack(HttpConnectionManager.newBuilder().setRds(rdsConfig).build()))) - ); - response = buildDiscoveryResponse("1", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0001"); - responseObserver.onNext(response); - - // Client sent a new RDS request with updated resource name. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "route-bar.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS, ""))); - - assertThat(rdsRespTimer.isCancelled()).isTrue(); - rdsRespTimer = - Iterables.getOnlyElement( - fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); - assertThat(rdsRespTimer.isCancelled()).isFalse(); - - // Management server sends back an RDS response containing RouteConfiguration requested. - List routeConfigs = ImmutableList.of( - Any.pack( - buildRouteConfiguration( - "route-bar.googleapis.com", // target route configuration - ImmutableList.of( - buildVirtualHost( - ImmutableList.of(TARGET_AUTHORITY), // matching virtual host - "cluster.googleapis.com"))))); - response = buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0000"); - responseObserver.onNext(response); - - assertThat(rdsRespTimer.isCancelled()).isTrue(); - } - - /** - * Client receives an CDS response that does not contain a Cluster for the requested resource - * while each received Cluster is valid. The CDS response is ACKed. Cluster watchers are notified - * with resource unavailable after initial resource fetch timeout has expired. - */ - @Test - public void cdsResponseWithoutMatchingResource() { - xdsClient.watchCdsResource("cluster-foo.googleapis.com", cdsResourceWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends a CDS request for the only cluster being watched to management server. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - - // Management server sends back a CDS response without Cluster for the requested resource. - List clusters = ImmutableList.of( - Any.pack(buildCluster("cluster-bar.googleapis.com", null, false)), - Any.pack(buildCluster("cluster-baz.googleapis.com", null, false))); - DiscoveryResponse response = - buildDiscoveryResponse("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS, "0000"); - responseObserver.onNext(response); - - // Client sent an ACK CDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, "0000"))); - verify(cdsResourceWatcher, never()).onChanged(any(CdsUpdate.class)); - verify(cdsResourceWatcher, never()).onResourceDoesNotExist("cluster-foo.googleapis.com"); - verify(cdsResourceWatcher, never()).onError(any(Status.class)); - - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(cdsResourceWatcher).onResourceDoesNotExist("cluster-foo.googleapis.com"); - assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); - } - - /** - * Normal workflow of receiving a CDS response containing Cluster message for a requested - * cluster. - */ - @Test - public void cdsResponseWithMatchingResource() { - xdsClient.watchCdsResource("cluster-foo.googleapis.com", cdsResourceWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends a CDS request for the only cluster being watched to management server. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - ScheduledTask cdsRespTimer = - Iterables.getOnlyElement( - fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); - - // Management server sends back a CDS response without Cluster for the requested resource. - List clusters = ImmutableList.of( - Any.pack(buildCluster("cluster-bar.googleapis.com", null, false)), - Any.pack(buildCluster("cluster-foo.googleapis.com", null, false)), - Any.pack(buildCluster("cluster-baz.googleapis.com", null, false))); - DiscoveryResponse response = - buildDiscoveryResponse("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS, "0000"); - responseObserver.onNext(response); - - // Client sent an ACK CDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, "0000"))); - assertThat(cdsRespTimer.isCancelled()).isTrue(); - - ArgumentCaptor cdsUpdateCaptor = ArgumentCaptor.forClass(null); - verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); - CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); - assertThat(cdsUpdate.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(cdsUpdate.getEdsServiceName()).isNull(); - assertThat(cdsUpdate.getLbPolicy()).isEqualTo("round_robin"); - assertThat(cdsUpdate.getLrsServerName()).isNull(); - - // Management server sends back another CDS response updating the requested Cluster. - clusters = ImmutableList.of( - Any.pack(buildCluster("cluster-bar.googleapis.com", null, false)), - Any.pack( - buildCluster("cluster-foo.googleapis.com", "eds-cluster-foo.googleapis.com", true)), - Any.pack(buildCluster("cluster-baz.googleapis.com", null, false))); - response = - buildDiscoveryResponse("1", clusters, XdsClientImpl.ADS_TYPE_URL_CDS, "0001"); - responseObserver.onNext(response); - - // Client sent an ACK CDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "1", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, "0001"))); - - verify(cdsResourceWatcher, times(2)).onChanged(cdsUpdateCaptor.capture()); - cdsUpdate = cdsUpdateCaptor.getValue(); - assertThat(cdsUpdate.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(cdsUpdate.getEdsServiceName()) - .isEqualTo("eds-cluster-foo.googleapis.com"); - assertThat(cdsUpdate.getLbPolicy()).isEqualTo("round_robin"); - assertThat(cdsUpdate.getLrsServerName()).isEqualTo(""); - } - - /** - * CDS response containing UpstreamTlsContext for a cluster. - */ - @Test - public void cdsResponseWithUpstreamTlsContext() { - xdsClient.watchCdsResource("cluster-foo.googleapis.com", cdsResourceWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Management server sends back CDS response with UpstreamTlsContext. - UpstreamTlsContext testUpstreamTlsContext = - buildUpstreamTlsContext("secret1", "unix:/var/uds2"); - List clusters = ImmutableList.of( - Any.pack(buildCluster("cluster-bar.googleapis.com", null, false)), - Any.pack(buildSecureCluster("cluster-foo.googleapis.com", - "eds-cluster-foo.googleapis.com", true, testUpstreamTlsContext)), - Any.pack(buildCluster("cluster-baz.googleapis.com", null, false))); - DiscoveryResponse response = - buildDiscoveryResponse("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS, "0000"); - responseObserver.onNext(response); - - // Client sent an ACK CDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, "0000"))); - ArgumentCaptor cdsUpdateCaptor = ArgumentCaptor.forClass(null); - verify(cdsResourceWatcher, times(1)).onChanged(cdsUpdateCaptor.capture()); - CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); - EnvoyServerProtoData.UpstreamTlsContext upstreamTlsContext = cdsUpdate - .getUpstreamTlsContext(); - SdsSecretConfig validationContextSdsSecretConfig = upstreamTlsContext.getCommonTlsContext() - .getValidationContextSdsSecretConfig(); - assertThat(validationContextSdsSecretConfig.getName()).isEqualTo("secret1"); - assertThat( - Iterables.getOnlyElement( - validationContextSdsSecretConfig - .getSdsConfig() - .getApiConfigSource() - .getGrpcServicesList()) - .getGoogleGrpc() - .getTargetUri()) - .isEqualTo("unix:/var/uds2"); - } - - @Test - public void multipleCdsWatchers() { - CdsResourceWatcher watcher1 = mock(CdsResourceWatcher.class); - CdsResourceWatcher watcher2 = mock(CdsResourceWatcher.class); - CdsResourceWatcher watcher3 = mock(CdsResourceWatcher.class); - xdsClient.watchCdsResource("cluster-foo.googleapis.com", watcher1); - xdsClient.watchCdsResource("cluster-foo.googleapis.com", watcher2); - xdsClient.watchCdsResource("cluster-bar.googleapis.com", watcher3); - - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends a CDS request containing all clusters being watched to management server. - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("", - ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(2); - - // Management server sends back a CDS response contains Cluster for only one of - // requested cluster. - List clusters = ImmutableList.of( - Any.pack(buildCluster("cluster-foo.googleapis.com", null, false))); - DiscoveryResponse response = - buildDiscoveryResponse("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS, "0000"); - responseObserver.onNext(response); - - assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - // Client sent an ACK CDS request. - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("0", - ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_CDS, "0000"))); - - // Two watchers get notification of cluster update for the cluster they are interested in. - ArgumentCaptor cdsUpdateCaptor1 = ArgumentCaptor.forClass(null); - verify(watcher1).onChanged(cdsUpdateCaptor1.capture()); - CdsUpdate cdsUpdate1 = cdsUpdateCaptor1.getValue(); - assertThat(cdsUpdate1.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(cdsUpdate1.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(cdsUpdate1.getEdsServiceName()).isNull(); - assertThat(cdsUpdate1.getLbPolicy()).isEqualTo("round_robin"); - assertThat(cdsUpdate1.getLrsServerName()).isNull(); - - ArgumentCaptor cdsUpdateCaptor2 = ArgumentCaptor.forClass(null); - verify(watcher2).onChanged(cdsUpdateCaptor2.capture()); - CdsUpdate cdsUpdate2 = cdsUpdateCaptor2.getValue(); - assertThat(cdsUpdate2.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(cdsUpdate2.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(cdsUpdate2.getEdsServiceName()).isNull(); - assertThat(cdsUpdate2.getLbPolicy()).isEqualTo("round_robin"); - assertThat(cdsUpdate2.getLrsServerName()).isNull(); - - verify(watcher3, never()).onChanged(any(CdsUpdate.class)); - verify(watcher3, never()).onResourceDoesNotExist("cluster-bar.googleapis.com"); - verify(watcher3, never()).onError(any(Status.class)); - - // The other watcher gets an error notification for cluster not found after its timer expired. - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(watcher3).onResourceDoesNotExist("cluster-bar.googleapis.com"); - assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); - - // Management server sends back another CDS response contains Clusters for all - // requested clusters. - clusters = ImmutableList.of( - Any.pack(buildCluster("cluster-foo.googleapis.com", null, false)), - Any.pack( - buildCluster("cluster-bar.googleapis.com", - "eds-cluster-bar.googleapis.com", true))); - response = buildDiscoveryResponse("1", clusters, - XdsClientImpl.ADS_TYPE_URL_CDS, "0001"); - responseObserver.onNext(response); - - // Client sent an ACK CDS request. - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("1", - ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_CDS, "0001"))); - - verifyNoMoreInteractions(watcher1, watcher2); // resource has no change - ArgumentCaptor cdsUpdateCaptor3 = ArgumentCaptor.forClass(null); - verify(watcher3).onChanged(cdsUpdateCaptor3.capture()); - CdsUpdate cdsUpdate3 = cdsUpdateCaptor3.getValue(); - assertThat(cdsUpdate3.getClusterName()).isEqualTo("cluster-bar.googleapis.com"); - assertThat(cdsUpdate3.getEdsServiceName()) - .isEqualTo("eds-cluster-bar.googleapis.com"); - assertThat(cdsUpdate3.getLbPolicy()).isEqualTo("round_robin"); - assertThat(cdsUpdate3.getLrsServerName()).isEqualTo(""); - } - - /** - * (CDS response caching behavior) Adding cluster watchers interested in some cluster that - * some other endpoint watcher had already been watching on will result in cluster update - * notified to the newly added watcher immediately, without sending new CDS requests. - */ - @Test - public void watchClusterAlreadyBeingWatched() { - CdsResourceWatcher watcher1 = mock(CdsResourceWatcher.class); - xdsClient.watchCdsResource("cluster-foo.googleapis.com", watcher1); - - // Streaming RPC starts after a first watcher is added. - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends an CDS request to management server. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - - // Management server sends back an CDS response with Cluster for the requested - // cluster. - List clusters = ImmutableList.of( - Any.pack(buildCluster("cluster-foo.googleapis.com", null, false))); - DiscoveryResponse response = - buildDiscoveryResponse("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS, "0000"); - responseObserver.onNext(response); - - // Client sent an ACK CDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, "0000"))); - - ArgumentCaptor cdsUpdateCaptor1 = ArgumentCaptor.forClass(null); - verify(watcher1).onChanged(cdsUpdateCaptor1.capture()); - CdsUpdate cdsUpdate1 = cdsUpdateCaptor1.getValue(); - assertThat(cdsUpdate1.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(cdsUpdate1.getEdsServiceName()).isNull(); - assertThat(cdsUpdate1.getLbPolicy()).isEqualTo("round_robin"); - assertThat(cdsUpdate1.getLrsServerName()).isNull(); - assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); - - // Another cluster watcher interested in the same cluster is added. - CdsResourceWatcher watcher2 = mock(CdsResourceWatcher.class); - xdsClient.watchCdsResource("cluster-foo.googleapis.com", watcher2); - - // Since the client has received cluster update for this cluster before, cached result is - // notified to the newly added watcher immediately. - ArgumentCaptor cdsUpdateCaptor2 = ArgumentCaptor.forClass(null); - verify(watcher2).onChanged(cdsUpdateCaptor2.capture()); - CdsUpdate cdsUpdate2 = cdsUpdateCaptor2.getValue(); - assertThat(cdsUpdate2.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(cdsUpdate2.getEdsServiceName()).isNull(); - assertThat(cdsUpdate2.getLbPolicy()).isEqualTo("round_robin"); - assertThat(cdsUpdate2.getLrsServerName()).isNull(); - - verifyNoMoreInteractions(requestObserver); - assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); - } - - /** - * Basic operations of adding/canceling cluster data watchers. - */ - @Test - public void addRemoveCdsWatchers() { - CdsResourceWatcher watcher1 = mock(CdsResourceWatcher.class); - xdsClient.watchCdsResource("cluster-foo.googleapis.com", watcher1); - - // Streaming RPC starts after a first watcher is added. - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends an CDS request to management server. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - - // Management server sends back a CDS response with Cluster for the requested - // cluster. - List clusters = ImmutableList.of( - Any.pack(buildCluster("cluster-foo.googleapis.com", null, false))); - DiscoveryResponse response = - buildDiscoveryResponse("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS, "0000"); - responseObserver.onNext(response); - - // Client sent an ACK CDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, "0000"))); - - ArgumentCaptor cdsUpdateCaptor1 = ArgumentCaptor.forClass(null); - verify(watcher1).onChanged(cdsUpdateCaptor1.capture()); - CdsUpdate cdsUpdate1 = cdsUpdateCaptor1.getValue(); - assertThat(cdsUpdate1.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(cdsUpdate1.getEdsServiceName()).isNull(); - assertThat(cdsUpdate1.getLbPolicy()).isEqualTo("round_robin"); - assertThat(cdsUpdate1.getLrsServerName()).isNull(); - - // Add another cluster watcher for a different cluster. - CdsResourceWatcher watcher2 = mock(CdsResourceWatcher.class); - xdsClient.watchCdsResource("cluster-bar.googleapis.com", watcher2); - - // Client sent a new CDS request for all interested resources. - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("0", - ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_CDS, "0000"))); - - // Management server sends back a CDS response with Cluster for all requested cluster. - clusters = ImmutableList.of( - Any.pack(buildCluster("cluster-foo.googleapis.com", null, false)), - Any.pack( - buildCluster("cluster-bar.googleapis.com", - "eds-cluster-bar.googleapis.com", true))); - response = buildDiscoveryResponse("1", clusters, - XdsClientImpl.ADS_TYPE_URL_CDS, "0001"); - responseObserver.onNext(response); - - // Client sent an ACK CDS request for all interested resources. - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("1", - ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_CDS, "0001"))); - verifyNoMoreInteractions(watcher1); // resource has no change - ArgumentCaptor cdsUpdateCaptor2 = ArgumentCaptor.forClass(null); - verify(watcher2).onChanged(cdsUpdateCaptor2.capture()); - CdsUpdate cdsUpdate2 = cdsUpdateCaptor2.getValue(); - assertThat(cdsUpdate2.getClusterName()).isEqualTo("cluster-bar.googleapis.com"); - assertThat(cdsUpdate2.getEdsServiceName()) - .isEqualTo("eds-cluster-bar.googleapis.com"); - assertThat(cdsUpdate2.getLbPolicy()).isEqualTo("round_robin"); - assertThat(cdsUpdate2.getLrsServerName()).isEqualTo(""); - - // Cancel one of the watcher. - xdsClient.cancelCdsResourceWatch("cluster-foo.googleapis.com", watcher1); - - // Since the cancelled watcher was the last watcher interested in that cluster (but there - // is still interested resource), client sent an new CDS request to unsubscribe from - // that cluster. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "1", "cluster-bar.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, "0001"))); - - // Management server has nothing to respond. - - // Cancel the other watcher. All resources have been unsubscribed. - xdsClient.cancelCdsResourceWatch("cluster-bar.googleapis.com", watcher2); - - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("1", ImmutableList.of(), - XdsClientImpl.ADS_TYPE_URL_CDS, "0001"))); - - // Management server sends back a new CDS response. - clusters = ImmutableList.of( - Any.pack(buildCluster("cluster-foo.googleapis.com", null, true)), - Any.pack( - buildCluster("cluster-bar.googleapis.com", null, false))); - response = - buildDiscoveryResponse("2", clusters, XdsClientImpl.ADS_TYPE_URL_CDS, "0002"); - responseObserver.onNext(response); - - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("2", ImmutableList.of(), - XdsClientImpl.ADS_TYPE_URL_CDS, "0002"))); - - // Cancelled watchers do not receive notification. - verifyNoMoreInteractions(watcher1, watcher2); - - // A new cluster watcher is added to watch cluster foo again. - CdsResourceWatcher watcher3 = mock(CdsResourceWatcher.class); - xdsClient.watchCdsResource("cluster-foo.googleapis.com", watcher3); - verify(watcher3, never()).onChanged(any(CdsUpdate.class)); - - // A CDS request is sent to indicate subscription of "cluster-foo.googleapis.com" only. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "2", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, "0002"))); - - // Management server sends back a new CDS response for at least newly requested resources - // (it is required to do so). - clusters = ImmutableList.of( - Any.pack(buildCluster("cluster-foo.googleapis.com", null, true)), - Any.pack( - buildCluster("cluster-bar.googleapis.com", null, false))); - response = - buildDiscoveryResponse("3", clusters, XdsClientImpl.ADS_TYPE_URL_CDS, "0003"); - responseObserver.onNext(response); - - // Notified with cached data immediately. - ArgumentCaptor cdsUpdateCaptor3 = ArgumentCaptor.forClass(null); - verify(watcher3).onChanged(cdsUpdateCaptor3.capture()); - CdsUpdate cdsUpdate3 = cdsUpdateCaptor3.getValue(); - assertThat(cdsUpdate3.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(cdsUpdate3.getEdsServiceName()).isNull(); - assertThat(cdsUpdate3.getLbPolicy()).isEqualTo("round_robin"); - assertThat(cdsUpdate2.getLrsServerName()).isEqualTo(""); - - verifyNoMoreInteractions(watcher1, watcher2); - - // A CDS request is sent to re-subscribe the cluster again. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "3", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, "0003"))); - } - - @Test - public void addRemoveCdsWatcherWhileInitialResourceFetchInProgress() { - CdsResourceWatcher watcher1 = mock(CdsResourceWatcher.class); - xdsClient.watchCdsResource("cluster-foo.googleapis.com", watcher1); - - // Streaming RPC starts after a first watcher is added. - StreamObserver requestObserver = requestObservers.poll(); - - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC - 1, TimeUnit.SECONDS); - - CdsResourceWatcher watcher2 = mock(CdsResourceWatcher.class); - CdsResourceWatcher watcher3 = mock(CdsResourceWatcher.class); - CdsResourceWatcher watcher4 = mock(CdsResourceWatcher.class); - xdsClient.watchCdsResource("cluster-foo.googleapis.com", watcher2); - xdsClient.watchCdsResource("cluster-bar.googleapis.com", watcher3); - xdsClient.watchCdsResource("cluster-bar.googleapis.com", watcher4); - - // Client sends a new CDS request for updating the latest resource subscription. - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("", - ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(2); - - fakeClock.forwardTime(1, TimeUnit.SECONDS); - assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - - // CDS resource "cluster-foo.googleapis.com" is known to be absent. - verify(watcher1).onResourceDoesNotExist("cluster-foo.googleapis.com"); - verify(watcher2).onResourceDoesNotExist("cluster-foo.googleapis.com"); - - // The absence result is known immediately. - CdsResourceWatcher watcher5 = mock(CdsResourceWatcher.class); - xdsClient.watchCdsResource("cluster-foo.googleapis.com", watcher5); - verify(watcher5).onResourceDoesNotExist("cluster-foo.googleapis.com"); - - assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - ScheduledTask timeoutTask = Iterables.getOnlyElement(fakeClock.getPendingTasks()); - - // Cancel watchers while discovery for resource "cluster-bar.googleapis.com" is still - // in progress. - xdsClient.cancelCdsResourceWatch("cluster-bar.googleapis.com", watcher3); - assertThat(timeoutTask.isCancelled()).isFalse(); - xdsClient.cancelCdsResourceWatch("cluster-bar.googleapis.com", watcher4); - - // Client sends a CDS request for resource subscription update (Omitted). - - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - - assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); - assertThat(timeoutTask.isCancelled()).isTrue(); - - verifyNoInteractions(watcher3, watcher4); - } - - @Test - public void cdsUpdateForClusterBeingRemoved() { - xdsClient.watchCdsResource("cluster-foo.googleapis.com", cdsResourceWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - - // Management server sends back a CDS response containing requested resource. - List clusters = ImmutableList.of( - Any.pack(buildCluster("cluster-foo.googleapis.com", null, true))); - DiscoveryResponse response = - buildDiscoveryResponse("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS, "0000"); - responseObserver.onNext(response); - - // Client sent an ACK CDS request (Omitted). - - ArgumentCaptor cdsUpdateCaptor = ArgumentCaptor.forClass(null); - verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); - CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); - assertThat(cdsUpdate.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(cdsUpdate.getEdsServiceName()).isNull(); - assertThat(cdsUpdate.getLbPolicy()).isEqualTo("round_robin"); - assertThat(cdsUpdate.getLrsServerName()).isEqualTo(""); - assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); - - // No cluster is available. - response = - buildDiscoveryResponse("1", ImmutableList.of(), - XdsClientImpl.ADS_TYPE_URL_CDS, "0001"); - responseObserver.onNext(response); - - verify(cdsResourceWatcher).onResourceDoesNotExist("cluster-foo.googleapis.com"); - } - - /** - * Client receives an EDS response that does not contain a ClusterLoadAssignment for the - * requested resource while each received ClusterLoadAssignment is valid. - * The EDS response is ACKed. - * After the resource fetch timeout expires, watchers waiting for the resource is notified - * with resource unavailable. - */ - @Test - public void edsResponseWithoutMatchingResource() { - xdsClient.watchEdsResource("cluster-foo.googleapis.com", edsResourceWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends an EDS request for the only cluster being watched to management server. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - - // Management server sends back an EDS response without ClusterLoadAssignment for the requested - // cluster. - List clusterLoadAssignments = ImmutableList.of( - Any.pack(buildClusterLoadAssignment("cluster-bar.googleapis.com", - ImmutableList.of( - buildLocalityLbEndpoints("region1", "zone1", "subzone1", - ImmutableList.of( - buildLbEndpoint("192.168.0.1", 8080, HealthStatus.HEALTHY, 2)), - 1, 0)), - ImmutableList.of())), - Any.pack(buildClusterLoadAssignment("cluster-baz.googleapis.com", - ImmutableList.of( - buildLocalityLbEndpoints("region2", "zone2", "subzone2", - ImmutableList.of( - buildLbEndpoint("192.168.234.52", 8888, HealthStatus.UNKNOWN, 5)), - 6, 1)), - ImmutableList.of()))); - - DiscoveryResponse response = - buildDiscoveryResponse("0", clusterLoadAssignments, - XdsClientImpl.ADS_TYPE_URL_EDS, "0000"); - responseObserver.onNext(response); - - // Client sent an ACK EDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, "0000"))); - - verify(edsResourceWatcher, never()).onChanged(any(EdsUpdate.class)); - verify(edsResourceWatcher, never()).onResourceDoesNotExist("cluster-foo.googleapis.com"); - verify(edsResourceWatcher, never()).onError(any(Status.class)); - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - verify(edsResourceWatcher).onResourceDoesNotExist("cluster-foo.googleapis.com"); - assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); - } - - /** - * Normal workflow of receiving an EDS response containing ClusterLoadAssignment message for - * a requested cluster. - */ - @Test - public void edsResponseWithMatchingResource() { - xdsClient.watchEdsResource("cluster-foo.googleapis.com", edsResourceWatcher); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends an EDS request for the only cluster being watched to management server. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - ScheduledTask edsRespTimeoutTask = - Iterables.getOnlyElement( - fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); - assertThat(edsRespTimeoutTask.isCancelled()).isFalse(); - - // Management server sends back an EDS response with ClusterLoadAssignment for the requested - // cluster. - List clusterLoadAssignments = ImmutableList.of( - Any.pack(buildClusterLoadAssignment("cluster-foo.googleapis.com", - ImmutableList.of( - buildLocalityLbEndpoints("region1", "zone1", "subzone1", - ImmutableList.of( - buildLbEndpoint("192.168.0.1", 8080, HealthStatus.HEALTHY, 2)), - 1, 0), - buildLocalityLbEndpoints("region3", "zone3", "subzone3", - ImmutableList.of(), - 2, 1), /* locality with 0 endpoint */ - buildLocalityLbEndpoints("region4", "zone4", "subzone4", - ImmutableList.of( - buildLbEndpoint("192.168.142.5", 80, HealthStatus.UNKNOWN, 5)), - 0, 2) /* locality with 0 weight */), - ImmutableList.of( - buildDropOverload("lb", 200), - buildDropOverload("throttle", 1000)))), - Any.pack(buildClusterLoadAssignment("cluster-baz.googleapis.com", - ImmutableList.of( - buildLocalityLbEndpoints("region2", "zone2", "subzone2", - ImmutableList.of( - buildLbEndpoint("192.168.234.52", 8888, HealthStatus.UNKNOWN, 5)), - 6, 1)), - ImmutableList.of()))); - - DiscoveryResponse response = - buildDiscoveryResponse("0", clusterLoadAssignments, - XdsClientImpl.ADS_TYPE_URL_EDS, "0000"); - responseObserver.onNext(response); - - assertThat(edsRespTimeoutTask.isCancelled()).isTrue(); - - // Client sent an ACK EDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, "0000"))); - - ArgumentCaptor edsUpdateCaptor = ArgumentCaptor.forClass(null); - verify(edsResourceWatcher).onChanged(edsUpdateCaptor.capture()); - EdsUpdate edsUpdate = edsUpdateCaptor.getValue(); - assertThat(edsUpdate.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(edsUpdate.getDropPolicies()) - .containsExactly( - new DropOverload("lb", 200), - new DropOverload("throttle", 1000)); - assertThat(edsUpdate.getLocalityLbEndpointsMap()) - .containsExactly( - new Locality("region1", "zone1", "subzone1"), - new LocalityLbEndpoints( - ImmutableList.of( - new LbEndpoint("192.168.0.1", 8080, - 2, true)), 1, 0), - new Locality("region3", "zone3", "subzone3"), - new LocalityLbEndpoints(ImmutableList.of(), 2, 1)); - - clusterLoadAssignments = ImmutableList.of( - Any.pack(buildClusterLoadAssignment("cluster-foo.googleapis.com", - // 0 locality - ImmutableList.of(), - ImmutableList.of()))); - response = - buildDiscoveryResponse( - "1", clusterLoadAssignments, XdsClientImpl.ADS_TYPE_URL_EDS, "0001"); - responseObserver.onNext(response); - - // Client sent an ACK EDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "1", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, "0001"))); - - verify(edsResourceWatcher, times(2)).onChanged(edsUpdateCaptor.capture()); - edsUpdate = edsUpdateCaptor.getValue(); - assertThat(edsUpdate.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(edsUpdate.getDropPolicies()).isEmpty(); - assertThat(edsUpdate.getLocalityLbEndpointsMap()).isEmpty(); - } - - @Test - public void multipleEdsWatchers() { - EdsResourceWatcher watcher1 = mock(EdsResourceWatcher.class); - EdsResourceWatcher watcher2 = mock(EdsResourceWatcher.class); - EdsResourceWatcher watcher3 = mock(EdsResourceWatcher.class); - xdsClient.watchEdsResource("cluster-foo.googleapis.com", watcher1); - xdsClient.watchEdsResource("cluster-foo.googleapis.com", watcher2); - xdsClient.watchEdsResource("cluster-bar.googleapis.com", watcher3); - - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends an EDS request containing all clusters being watched to management server. - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("", - ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - - assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(2); - - // Management server sends back an EDS response contains ClusterLoadAssignment for only one of - // requested cluster. - List clusterLoadAssignments = ImmutableList.of( - Any.pack(buildClusterLoadAssignment("cluster-foo.googleapis.com", - ImmutableList.of( - buildLocalityLbEndpoints("region1", "zone1", "subzone1", - ImmutableList.of( - buildLbEndpoint("192.168.0.1", 8080, HealthStatus.HEALTHY, 2)), - 1, 0)), - ImmutableList.of()))); - - DiscoveryResponse response = - buildDiscoveryResponse("0", clusterLoadAssignments, - XdsClientImpl.ADS_TYPE_URL_EDS, "0000"); - responseObserver.onNext(response); - - assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - - // Client sent an ACK EDS request. - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("0", - ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_EDS, "0000"))); - - // Two watchers get notification of endpoint update for the cluster they are interested in. - ArgumentCaptor edsUpdateCaptor1 = ArgumentCaptor.forClass(null); - verify(watcher1).onChanged(edsUpdateCaptor1.capture()); - EdsUpdate edsUpdate1 = edsUpdateCaptor1.getValue(); - assertThat(edsUpdate1.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(edsUpdate1.getLocalityLbEndpointsMap()) - .containsExactly( - new Locality("region1", "zone1", "subzone1"), - new LocalityLbEndpoints( - ImmutableList.of( - new LbEndpoint("192.168.0.1", 8080, - 2, true)), 1, 0)); - - ArgumentCaptor edsUpdateCaptor2 = ArgumentCaptor.forClass(null); - verify(watcher1).onChanged(edsUpdateCaptor2.capture()); - EdsUpdate edsUpdate2 = edsUpdateCaptor2.getValue(); - assertThat(edsUpdate2.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(edsUpdate2.getLocalityLbEndpointsMap()) - .containsExactly( - new Locality("region1", "zone1", "subzone1"), - new LocalityLbEndpoints( - ImmutableList.of( - new LbEndpoint("192.168.0.1", 8080, - 2, true)), 1, 0)); - - verifyNoInteractions(watcher3); - - // Management server sends back another EDS response contains ClusterLoadAssignment for the - // other requested cluster. - clusterLoadAssignments = ImmutableList.of( - Any.pack(buildClusterLoadAssignment("cluster-bar.googleapis.com", - ImmutableList.of( - buildLocalityLbEndpoints("region2", "zone2", "subzone2", - ImmutableList.of( - buildLbEndpoint("192.168.234.52", 8888, HealthStatus.UNKNOWN, 5)), - 6, 0)), - ImmutableList.of()))); - - response = buildDiscoveryResponse("1", clusterLoadAssignments, - XdsClientImpl.ADS_TYPE_URL_EDS, "0001"); - responseObserver.onNext(response); - - // Client sent an ACK EDS request. - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("1", - ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_EDS, "0001"))); - - // The corresponding watcher gets notified. - ArgumentCaptor edsUpdateCaptor3 = ArgumentCaptor.forClass(null); - verify(watcher3).onChanged(edsUpdateCaptor3.capture()); - EdsUpdate edsUpdate3 = edsUpdateCaptor3.getValue(); - assertThat(edsUpdate3.getClusterName()).isEqualTo("cluster-bar.googleapis.com"); - assertThat(edsUpdate3.getLocalityLbEndpointsMap()) - .containsExactly( - new Locality("region2", "zone2", "subzone2"), - new LocalityLbEndpoints( - ImmutableList.of( - new LbEndpoint("192.168.234.52", 8888, - 5, true)), 6, 0)); - } - - /** - * (EDS response caching behavior) An endpoint watcher is registered for a cluster that already - * has some other endpoint watchers watching on. Endpoint information received previously is - * in local cache and notified to the new watcher immediately. - */ - @Test - public void watchEndpointsForClusterAlreadyBeingWatched() { - EdsResourceWatcher watcher1 = mock(EdsResourceWatcher.class); - xdsClient.watchEdsResource("cluster-foo.googleapis.com", watcher1); - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends first EDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - - assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - - // Management server sends back an EDS response containing ClusterLoadAssignments for - // some cluster not requested. - List clusterLoadAssignments = ImmutableList.of( - Any.pack(buildClusterLoadAssignment("cluster-foo.googleapis.com", - ImmutableList.of( - buildLocalityLbEndpoints("region1", "zone1", "subzone1", - ImmutableList.of( - buildLbEndpoint("192.168.0.1", 8080, HealthStatus.HEALTHY, 2)), - 1, 0)), - ImmutableList.of()))); - - DiscoveryResponse response = - buildDiscoveryResponse("0", clusterLoadAssignments, - XdsClientImpl.ADS_TYPE_URL_EDS, "0000"); - responseObserver.onNext(response); - - assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); - - // Client sent an ACK EDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, "0000"))); - - ArgumentCaptor edsUpdateCaptor1 = ArgumentCaptor.forClass(null); - verify(watcher1).onChanged(edsUpdateCaptor1.capture()); - EdsUpdate edsUpdate1 = edsUpdateCaptor1.getValue(); - assertThat(edsUpdate1.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(edsUpdate1.getDropPolicies()).isEmpty(); - assertThat(edsUpdate1.getLocalityLbEndpointsMap()) - .containsExactly( - new Locality("region1", "zone1", "subzone1"), - new LocalityLbEndpoints( - ImmutableList.of( - new LbEndpoint("192.168.0.1", 8080, - 2, true)), 1, 0)); - - // A second endpoint watcher is registered for endpoints in the same cluster. - EdsResourceWatcher watcher2 = mock(EdsResourceWatcher.class); - xdsClient.watchEdsResource("cluster-foo.googleapis.com", watcher2); - - // Cached endpoint information is notified to the new watcher immediately, without sending - // another EDS request. - ArgumentCaptor edsUpdateCaptor2 = ArgumentCaptor.forClass(null); - verify(watcher2).onChanged(edsUpdateCaptor2.capture()); - EdsUpdate edsUpdate2 = edsUpdateCaptor2.getValue(); - assertThat(edsUpdate2.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(edsUpdate2.getDropPolicies()).isEmpty(); - assertThat(edsUpdate2.getLocalityLbEndpointsMap()) - .containsExactly( - new Locality("region1", "zone1", "subzone1"), - new LocalityLbEndpoints( - ImmutableList.of( - new LbEndpoint("192.168.0.1", 8080, - 2, true)), 1, 0)); - - verifyNoMoreInteractions(requestObserver); - assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); - } - - /** - * Basic operations of adding/canceling endpoint data watchers. - */ - @Test - public void addRemoveEdsWatchers() { - EdsResourceWatcher watcher1 = mock(EdsResourceWatcher.class); - xdsClient.watchEdsResource("cluster-foo.googleapis.com", watcher1); - - // Streaming RPC starts after a first watcher is added. - StreamObserver responseObserver = responseObservers.poll(); - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends an EDS request to management server. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - - // Management server sends back an EDS response with ClusterLoadAssignment for the requested - // cluster. - List clusterLoadAssignments = ImmutableList.of( - Any.pack(buildClusterLoadAssignment("cluster-foo.googleapis.com", - ImmutableList.of( - buildLocalityLbEndpoints("region1", "zone1", "subzone1", - ImmutableList.of( - buildLbEndpoint("192.168.0.1", 8080, HealthStatus.HEALTHY, 2), - buildLbEndpoint("192.132.53.5", 80, HealthStatus.UNHEALTHY, 5)), - 1, 0)), - ImmutableList.of()))); - - DiscoveryResponse response = - buildDiscoveryResponse("0", clusterLoadAssignments, - XdsClientImpl.ADS_TYPE_URL_EDS, "0000"); - responseObserver.onNext(response); - - // Client sent an ACK EDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, "0000"))); - - ArgumentCaptor edsUpdateCaptor1 = ArgumentCaptor.forClass(null); - verify(watcher1).onChanged(edsUpdateCaptor1.capture()); - EdsUpdate edsUpdate1 = edsUpdateCaptor1.getValue(); - assertThat(edsUpdate1.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); - assertThat(edsUpdate1.getLocalityLbEndpointsMap()) - .containsExactly( - new Locality("region1", "zone1", "subzone1"), - new LocalityLbEndpoints( - ImmutableList.of( - new LbEndpoint("192.168.0.1", 8080, 2, true), - new LbEndpoint("192.132.53.5", 80,5, false)), - 1, 0)); - - // Add another endpoint watcher for a different cluster. - EdsResourceWatcher watcher2 = mock(EdsResourceWatcher.class); - xdsClient.watchEdsResource("cluster-bar.googleapis.com", watcher2); - - // Client sent a new EDS request for all interested resources. - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("0", - ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_EDS, "0000"))); - - // Management server sends back an EDS response with ClusterLoadAssignment for one of requested - // cluster. - clusterLoadAssignments = ImmutableList.of( - Any.pack(buildClusterLoadAssignment("cluster-bar.googleapis.com", - ImmutableList.of( - buildLocalityLbEndpoints("region2", "zone2", "subzone2", - ImmutableList.of( - buildLbEndpoint("192.168.312.6", 443, HealthStatus.HEALTHY, 1)), - 6, 0)), - ImmutableList.of()))); - - response = buildDiscoveryResponse("1", clusterLoadAssignments, - XdsClientImpl.ADS_TYPE_URL_EDS, "0001"); - responseObserver.onNext(response); - - // Client sent an ACK EDS request for all interested resources. - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("1", - ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_EDS, "0001"))); - - ArgumentCaptor edsUpdateCaptor2 = ArgumentCaptor.forClass(null); - verify(watcher2).onChanged(edsUpdateCaptor2.capture()); - EdsUpdate edsUpdate2 = edsUpdateCaptor2.getValue(); - assertThat(edsUpdate2.getClusterName()).isEqualTo("cluster-bar.googleapis.com"); - assertThat(edsUpdate2.getLocalityLbEndpointsMap()) - .containsExactly( - new Locality("region2", "zone2", "subzone2"), - new LocalityLbEndpoints( - ImmutableList.of( - new LbEndpoint("192.168.312.6", 443, 1, true)), - 6, 0)); - - // Cancel one of the watcher. - xdsClient.cancelEdsResourceWatch("cluster-foo.googleapis.com", watcher1); - - // Since the cancelled watcher was the last watcher interested in that cluster, client - // sent an new EDS request to unsubscribe from that cluster. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "1", "cluster-bar.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, "0001"))); - - // Management server should not respond as it had previously sent the requested resource. - - // Cancel the other watcher. - xdsClient.cancelEdsResourceWatch("cluster-bar.googleapis.com", watcher2); - - // Since the cancelled watcher was the last watcher interested in that cluster, client - // sent an new EDS request to unsubscribe from that cluster. - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("1", - ImmutableList.of(), // empty resources - XdsClientImpl.ADS_TYPE_URL_EDS, "0001"))); - - // All endpoint watchers have been cancelled. - - // Management server sends back an EDS response for updating previously sent resources. - clusterLoadAssignments = ImmutableList.of( - Any.pack(buildClusterLoadAssignment("cluster-foo.googleapis.com", - ImmutableList.of( - buildLocalityLbEndpoints("region3", "zone3", "subzone3", - ImmutableList.of( - buildLbEndpoint("192.168.432.6", 80, HealthStatus.HEALTHY, 2)), - 3, 0)), - ImmutableList.of())), - Any.pack(buildClusterLoadAssignment("cluster-bar.googleapis.com", - ImmutableList.of( - buildLocalityLbEndpoints("region4", "zone4", "subzone4", - ImmutableList.of( - buildLbEndpoint("192.168.75.6", 8888, HealthStatus.HEALTHY, 2)), - 3, 0)), - ImmutableList.of()))); - - response = buildDiscoveryResponse("2", clusterLoadAssignments, - XdsClientImpl.ADS_TYPE_URL_EDS, "0002"); - responseObserver.onNext(response); - - // Client sent an ACK EDS request. - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("2", - ImmutableList.of(), // empty resources - XdsClientImpl.ADS_TYPE_URL_EDS, "0002"))); - - // Cancelled watchers do not receive notification. - verifyNoMoreInteractions(watcher1, watcher2); - - // A new endpoint watcher is added to watch an old but was no longer interested in cluster. - EdsResourceWatcher watcher3 = mock(EdsResourceWatcher.class); - xdsClient.watchEdsResource("cluster-bar.googleapis.com", watcher3); - - // Nothing should be notified to the new watcher as we are still waiting management server's - // latest response. - // Cached endpoint data should have been purged. - verify(watcher3, never()).onChanged(any(EdsUpdate.class)); - - // An EDS request is sent to re-subscribe the cluster again. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "2", "cluster-bar.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, "0002"))); - - // Management server sends back an EDS response for re-subscribed resource. - clusterLoadAssignments = ImmutableList.of( - Any.pack(buildClusterLoadAssignment("cluster-bar.googleapis.com", - ImmutableList.of( - buildLocalityLbEndpoints("region4", "zone4", "subzone4", - ImmutableList.of( - buildLbEndpoint("192.168.75.6", 8888, HealthStatus.HEALTHY, 2)), - 3, 0)), - ImmutableList.of()))); - - response = buildDiscoveryResponse("3", clusterLoadAssignments, - XdsClientImpl.ADS_TYPE_URL_EDS, "0003"); - responseObserver.onNext(response); - - ArgumentCaptor edsUpdateCaptor3 = ArgumentCaptor.forClass(null); - verify(watcher3).onChanged(edsUpdateCaptor3.capture()); - EdsUpdate edsUpdate3 = edsUpdateCaptor3.getValue(); - assertThat(edsUpdate3.getClusterName()).isEqualTo("cluster-bar.googleapis.com"); - assertThat(edsUpdate3.getLocalityLbEndpointsMap()) - .containsExactly( - new Locality("region4", "zone4", "subzone4"), - new LocalityLbEndpoints( - ImmutableList.of( - new LbEndpoint("192.168.75.6", 8888, 2, true)), - 3, 0)); - - // Client sent an ACK EDS request. - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("3", - ImmutableList.of("cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_EDS, "0003"))); - } - - @Test - public void addRemoveEdsWatcherWhileInitialResourceFetchInProgress() { - EdsResourceWatcher watcher1 = mock(EdsResourceWatcher.class); - xdsClient.watchEdsResource("cluster-foo.googleapis.com", watcher1); - - // Streaming RPC starts after a first watcher is added. - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends an EDS request to management server. - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC - 1, TimeUnit.SECONDS); - - EdsResourceWatcher watcher2 = mock(EdsResourceWatcher.class); - EdsResourceWatcher watcher3 = mock(EdsResourceWatcher.class); - EdsResourceWatcher watcher4 = mock(EdsResourceWatcher.class); - xdsClient.watchEdsResource("cluster-foo.googleapis.com", watcher2); - xdsClient.watchEdsResource("cluster-bar.googleapis.com", watcher3); - xdsClient.watchEdsResource("cluster-bar.googleapis.com", watcher4); - - // Client sends a new EDS request for updating the latest resource subscription. - verify(requestObserver) - .onNext( - argThat( - new DiscoveryRequestMatcher("", - ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(2); - - fakeClock.forwardTime(1, TimeUnit.SECONDS); - assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - - // EDS resource "cluster-foo.googleapis.com" is known to be absent. - verify(watcher1).onResourceDoesNotExist("cluster-foo.googleapis.com"); - verify(watcher2).onResourceDoesNotExist("cluster-foo.googleapis.com"); - - // The absence result is known immediately. - EdsResourceWatcher watcher5 = mock(EdsResourceWatcher.class); - xdsClient.watchEdsResource("cluster-foo.googleapis.com", watcher5); - verify(watcher5).onResourceDoesNotExist("cluster-foo.googleapis.com"); - - assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - ScheduledTask timeoutTask = Iterables.getOnlyElement(fakeClock.getPendingTasks()); - - // Cancel watchers while discovery for resource "cluster-bar.googleapis.com" is still - // in progress. - xdsClient.cancelEdsResourceWatch("cluster-bar.googleapis.com", watcher3); - assertThat(timeoutTask.isCancelled()).isFalse(); - xdsClient.cancelEdsResourceWatch("cluster-bar.googleapis.com", watcher4); - - // Client sends an EDS request for resource subscription update (Omitted). - - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); - - assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); - assertThat(timeoutTask.isCancelled()).isTrue(); - - verifyNoInteractions(watcher3, watcher4); - } - - @Test - public void cdsUpdateForEdsServiceNameChange() { - xdsClient.watchCdsResource("cluster-foo.googleapis.com", cdsResourceWatcher); - StreamObserver responseObserver = responseObservers.poll(); - - // Management server sends back a CDS response containing requested resource. - List clusters = ImmutableList.of( - Any.pack(buildCluster("cluster-foo.googleapis.com", "cluster-foo:service-bar", false))); - DiscoveryResponse response = - buildDiscoveryResponse("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS, "0000"); - responseObserver.onNext(response); - - xdsClient.watchEdsResource("cluster-foo:service-bar", edsResourceWatcher); - - // Management server sends back an EDS response for resource "cluster-foo:service-bar". - List clusterLoadAssignments = ImmutableList.of( - Any.pack(buildClusterLoadAssignment("cluster-foo:service-bar", - ImmutableList.of( - buildLocalityLbEndpoints("region1", "zone1", "subzone1", - ImmutableList.of( - buildLbEndpoint("192.168.0.1", 8080, HealthStatus.HEALTHY, 2)), - 1, 0)), - ImmutableList.of()))); - response = - buildDiscoveryResponse("0", clusterLoadAssignments, - XdsClientImpl.ADS_TYPE_URL_EDS, "0000"); - responseObserver.onNext(response); - - ArgumentCaptor edsUpdateCaptor = ArgumentCaptor.forClass(null); - verify(edsResourceWatcher).onChanged(edsUpdateCaptor.capture()); - EdsUpdate edsUpdate = edsUpdateCaptor.getValue(); - assertThat(edsUpdate.getClusterName()).isEqualTo("cluster-foo:service-bar"); - assertThat(edsUpdate.getDropPolicies()).isEmpty(); - assertThat(edsUpdate.getLocalityLbEndpointsMap()) - .containsExactly( - new Locality("region1", "zone1", "subzone1"), - new LocalityLbEndpoints( - ImmutableList.of( - new LbEndpoint("192.168.0.1", 8080, - 2, true)), 1, 0)); - - // Management server sends another CDS response for removing cluster service - // "cluster-foo:service-blade" with replacement of "cluster-foo:service-blade". - clusters = ImmutableList.of( - Any.pack(buildCluster("cluster-foo.googleapis.com", "cluster-foo:service-blade", false))); - response = - buildDiscoveryResponse("1", clusters, XdsClientImpl.ADS_TYPE_URL_CDS, "0001"); - responseObserver.onNext(response); - - // Watcher get notification for endpoint resource "cluster-foo:service-bar" being deleted. - verify(edsResourceWatcher).onResourceDoesNotExist("cluster-foo:service-bar"); - } - - /** - * RPC stream closed and retry during the period of first time resolving service config - * (LDS/RDS only). - */ - @Test - public void streamClosedAndRetryWhenResolvingConfig() { - InOrder inOrder = - Mockito.inOrder(mockedDiscoveryService, backoffPolicyProvider, backoffPolicy1, - backoffPolicy2); - xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher); - - ArgumentCaptor> responseObserverCaptor = - ArgumentCaptor.forClass(null); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - StreamObserver responseObserver = - responseObserverCaptor.getValue(); // same as responseObservers.poll() - StreamObserver requestObserver = requestObservers.poll(); - - // Client sends an LDS request for the host name (with port) to management server. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - - // Management server closes the RPC stream immediately. - responseObserver.onCompleted(); - inOrder.verify(backoffPolicyProvider).get(); - inOrder.verify(backoffPolicy1).nextBackoffNanos(); - assertThat(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)).hasSize(1); - - // Retry after backoff. - fakeClock.forwardNanos(9L); - assertThat(requestObservers).isEmpty(); - fakeClock.forwardNanos(1L); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - responseObserver = responseObserverCaptor.getValue(); - requestObserver = requestObservers.poll(); - - // Client retried by sending an LDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - - // Management server closes the RPC stream with an error. - responseObserver.onError(Status.UNAVAILABLE.asException()); - verifyNoMoreInteractions(backoffPolicyProvider); - inOrder.verify(backoffPolicy1).nextBackoffNanos(); - assertThat(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)).hasSize(1); - - // Retry after backoff. - fakeClock.forwardNanos(99L); - assertThat(requestObservers).isEmpty(); - fakeClock.forwardNanos(1L); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - responseObserver = responseObserverCaptor.getValue(); - requestObserver = requestObservers.poll(); - - // Client retried again by sending an LDS. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - - // Management server responses with a listener for the requested resource. - Rds rdsConfig = - Rds.newBuilder() - .setConfigSource( - ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) - .setRouteConfigName("route-foo.googleapis.com") - .build(); - - List listeners = ImmutableList.of( - Any.pack(buildListener(TARGET_AUTHORITY, /* matching resource */ - Any.pack(HttpConnectionManager.newBuilder().setRds(rdsConfig).build()))) - ); - DiscoveryResponse ldsResponse = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000"); - responseObserver.onNext(ldsResponse); - - // Client sent back an ACK LDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, "0000"))); - - // Client sent an RDS request based on the received listener. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS, ""))); - - // Management server encounters an error and closes the stream. - responseObserver.onError(Status.UNKNOWN.asException()); - - // Reset backoff and retry immediately. - inOrder.verify(backoffPolicyProvider).get(); - fakeClock.runDueTasks(); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - responseObserver = responseObserverCaptor.getValue(); - requestObserver = requestObservers.poll(); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - - // RPC stream closed immediately - responseObserver.onError(Status.UNKNOWN.asException()); - inOrder.verify(backoffPolicy2).nextBackoffNanos(); - assertThat(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)).hasSize(1); - - // Retry after backoff. - fakeClock.forwardNanos(19L); - assertThat(requestObservers).isEmpty(); - fakeClock.forwardNanos(1L); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - responseObserver = responseObserverCaptor.getValue(); - requestObserver = requestObservers.poll(); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - - // Management server sends an LDS response. - ldsResponse = buildDiscoveryResponse("1", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0001"); - responseObserver.onNext(ldsResponse); - - // Client sends an ACK LDS request and an RDS request for "route-foo.googleapis.com". (Omitted) - - List routeConfigs = ImmutableList.of( - Any.pack( - buildRouteConfiguration( - "route-foo.googleapis.com", // target route configuration - ImmutableList.of( - buildVirtualHost( - ImmutableList.of(TARGET_AUTHORITY), // matching virtual host - "cluster.googleapis.com"))))); - DiscoveryResponse rdsResponse = - buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0000"); - // Management server sends an RDS response. - responseObserver.onNext(rdsResponse); - - // Client has resolved the cluster based on the RDS response. - ArgumentCaptor configUpdateCaptor = ArgumentCaptor.forClass(null); - verify(configWatcher).onConfigChanged(configUpdateCaptor.capture()); - assertConfigUpdateContainsSingleClusterRoute( - configUpdateCaptor.getValue(), "cluster.googleapis.com"); - - // RPC stream closed with an error again. - responseObserver.onError(Status.UNKNOWN.asException()); - - // Reset backoff and retry immediately. - inOrder.verify(backoffPolicyProvider).get(); - fakeClock.runDueTasks(); - requestObserver = requestObservers.poll(); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "1", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - - verifyNoMoreInteractions(backoffPolicyProvider, backoffPolicy1, backoffPolicy2); - } - - /** - * RPC stream close and retry while there are config/cluster/endpoint watchers registered. - */ - @Test - public void streamClosedAndRetry() { - InOrder inOrder = - Mockito.inOrder(mockedDiscoveryService, backoffPolicyProvider, backoffPolicy1, - backoffPolicy2); - xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher); - - ArgumentCaptor> responseObserverCaptor = - ArgumentCaptor.forClass(null); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - StreamObserver responseObserver = - responseObserverCaptor.getValue(); // same as responseObservers.poll() - StreamObserver requestObserver = requestObservers.poll(); - - waitUntilConfigResolved(responseObserver); - ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(null); - - // Start watching cluster information. - xdsClient.watchCdsResource("cluster.googleapis.com", cdsResourceWatcher); - - // Client sent first CDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - - // Start watching endpoint information. - xdsClient.watchEdsResource("cluster.googleapis.com", edsResourceWatcher); - - // Client sent first EDS request. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - - // Management server closes the RPC stream with an error. - responseObserver.onError(Status.UNKNOWN.asException()); - verify(configWatcher).onError(statusCaptor.capture()); - assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNKNOWN); - verify(cdsResourceWatcher).onError(statusCaptor.capture()); - assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNKNOWN); - verify(edsResourceWatcher).onError(statusCaptor.capture()); - assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNKNOWN); - - // Resets backoff and retry immediately. - inOrder.verify(backoffPolicyProvider).get(); - fakeClock.runDueTasks(); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - responseObserver = responseObserverCaptor.getValue(); - requestObserver = requestObservers.poll(); - - // Retry resumes requests for all wanted resources. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - - // Management server becomes unreachable. - responseObserver.onError(Status.UNAVAILABLE.asException()); - verify(configWatcher, times(2)).onError(statusCaptor.capture()); - assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNAVAILABLE); - verify(cdsResourceWatcher, times(2)).onError(statusCaptor.capture()); - assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNAVAILABLE); - verify(edsResourceWatcher, times(2)).onError(statusCaptor.capture()); - assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNAVAILABLE); - inOrder.verify(backoffPolicy1).nextBackoffNanos(); - assertThat(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)).hasSize(1); - - // Retry after backoff. - fakeClock.forwardNanos(9L); - assertThat(requestObservers).isEmpty(); - fakeClock.forwardNanos(1L); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - responseObserver = responseObserverCaptor.getValue(); - requestObserver = requestObservers.poll(); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - - // Management server is still not reachable. - responseObserver.onError(Status.UNAVAILABLE.asException()); - verify(configWatcher, times(3)).onError(statusCaptor.capture()); - assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNAVAILABLE); - verify(cdsResourceWatcher, times(3)).onError(statusCaptor.capture()); - assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNAVAILABLE); - verify(edsResourceWatcher, times(3)).onError(statusCaptor.capture()); - assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNAVAILABLE); - inOrder.verify(backoffPolicy1).nextBackoffNanos(); - assertThat(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)).hasSize(1); - - // Retry after backoff. - fakeClock.forwardNanos(99L); - assertThat(requestObservers).isEmpty(); - fakeClock.forwardNanos(1L); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - responseObserver = responseObserverCaptor.getValue(); - requestObserver = requestObservers.poll(); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - - // Management server sends back a CDS response. - List clusters = ImmutableList.of( - Any.pack(buildCluster("cluster.googleapis.com", null, false))); - DiscoveryResponse cdsResponse = - buildDiscoveryResponse("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS, "0000"); - responseObserver.onNext(cdsResponse); - - // Client sent an CDS ACK request (Omitted). - - // Management server closes the RPC stream. - responseObserver.onCompleted(); - verify(configWatcher, times(4)).onError(any(Status.class)); - verify(cdsResourceWatcher, times(4)).onError(any(Status.class)); - verify(edsResourceWatcher, times(4)).onError(any(Status.class)); - - // Resets backoff and retry immediately - inOrder.verify(backoffPolicyProvider).get(); - fakeClock.runDueTasks(); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - responseObserver = responseObserverCaptor.getValue(); - requestObserver = requestObservers.poll(); - - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - - // Management server becomes unreachable again. - responseObserver.onError(Status.UNAVAILABLE.asException()); - verify(configWatcher, times(5)).onError(statusCaptor.capture()); - assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNAVAILABLE); - verify(cdsResourceWatcher, times(5)).onError(statusCaptor.capture()); - assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNAVAILABLE); - verify(edsResourceWatcher, times(5)).onError(statusCaptor.capture()); - assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNAVAILABLE); - inOrder.verify(backoffPolicy2).nextBackoffNanos(); - assertThat(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)).hasSize(1); - - // Retry after backoff. - fakeClock.forwardNanos(19L); - assertThat(requestObservers).isEmpty(); - fakeClock.forwardNanos(1L); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - requestObserver = requestObservers.poll(); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - - verifyNoMoreInteractions(mockedDiscoveryService, backoffPolicyProvider, backoffPolicy1, - backoffPolicy2); - } - - /** - * RPC stream closed and retry while some cluster/endpoint watchers have changed (added/removed). - */ - @Test - public void streamClosedAndRetryRaceWithAddingAndRemovingWatchers() { - InOrder inOrder = - Mockito.inOrder(mockedDiscoveryService, backoffPolicyProvider, backoffPolicy1, - backoffPolicy2); - xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher); - - ArgumentCaptor> responseObserverCaptor = - ArgumentCaptor.forClass(null); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - StreamObserver responseObserver = - responseObserverCaptor.getValue(); // same as responseObservers.poll() - requestObservers.poll(); - - waitUntilConfigResolved(responseObserver); - - // Management server closes RPC stream. - responseObserver.onCompleted(); - - // Resets backoff and retry immediately. - inOrder.verify(backoffPolicyProvider).get(); - fakeClock.runDueTasks(); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - responseObserver = responseObserverCaptor.getValue(); - StreamObserver requestObserver = requestObservers.poll(); - - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - - // Management server becomes unreachable. - responseObserver.onError(Status.UNAVAILABLE.asException()); - inOrder.verify(backoffPolicy1).nextBackoffNanos(); - assertThat(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)).hasSize(1); - - // Start watching cluster information while RPC stream is still in retry backoff. - xdsClient.watchCdsResource("cluster.googleapis.com", cdsResourceWatcher); - - // Retry after backoff. - fakeClock.forwardNanos(9L); - assertThat(requestObservers).isEmpty(); - fakeClock.forwardNanos(1L); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - responseObserver = responseObserverCaptor.getValue(); - requestObserver = requestObservers.poll(); - - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - - // Management server is still unreachable. - responseObserver.onError(Status.UNAVAILABLE.asException()); - inOrder.verify(backoffPolicy1).nextBackoffNanos(); - assertThat(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)).hasSize(1); - - // Start watching endpoint information while RPC stream is still in retry backoff. - xdsClient.watchEdsResource("cluster.googleapis.com", edsResourceWatcher); - - // Retry after backoff. - fakeClock.forwardNanos(99L); - assertThat(requestObservers).isEmpty(); - fakeClock.forwardNanos(1L); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - responseObserver = responseObserverCaptor.getValue(); - requestObserver = requestObservers.poll(); - - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - - // Management server sends back a CDS response. - List clusters = ImmutableList.of( - Any.pack(buildCluster("cluster.googleapis.com", null, false))); - DiscoveryResponse cdsResponse = - buildDiscoveryResponse("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS, "0000"); - responseObserver.onNext(cdsResponse); - - // Client sent an CDS ACK request (Omitted). - - // No longer interested in endpoint information after RPC resumes. - xdsClient.cancelEdsResourceWatch("cluster.googleapis.com", edsResourceWatcher); - // Client updates EDS resource subscription immediately. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", ImmutableList.of(), - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - - // Become interested in endpoints of another cluster. - xdsClient.watchEdsResource("cluster2.googleapis.com", edsResourceWatcher); - // Client updates EDS resource subscription immediately. - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster2.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - - // Management server closes the RPC stream again. - responseObserver.onCompleted(); - - // Resets backoff and retry immediately. - inOrder.verify(backoffPolicyProvider).get(); - fakeClock.runDueTasks(); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - responseObserver = responseObserverCaptor.getValue(); - requestObserver = requestObservers.poll(); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster2.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - - // Management server becomes unreachable again. - responseObserver.onError(Status.UNAVAILABLE.asException()); - inOrder.verify(backoffPolicy2).nextBackoffNanos(); - assertThat(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)).hasSize(1); - - // No longer interested in previous cluster and endpoints in that cluster. - xdsClient.cancelCdsResourceWatch("cluster.googleapis.com", cdsResourceWatcher); - xdsClient.cancelEdsResourceWatch("cluster2.googleapis.com", edsResourceWatcher); - - // Retry after backoff. - fakeClock.forwardNanos(19L); - assertThat(requestObservers).isEmpty(); - fakeClock.forwardNanos(1L); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - requestObserver = requestObservers.poll(); - - verify(requestObserver) - .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - verify(requestObserver, never()) - .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS, ""))); - verify(requestObserver, never()) - .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster2.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS, ""))); - - verifyNoMoreInteractions(mockedDiscoveryService, backoffPolicyProvider, backoffPolicy1, - backoffPolicy2); - } - - @Test - public void streamClosedAndRetryReschedulesAllResourceFetchTimer() { - InOrder inOrder = - Mockito.inOrder(mockedDiscoveryService, backoffPolicyProvider, backoffPolicy1, - backoffPolicy2); - xdsClient.watchConfigData(TARGET_AUTHORITY, configWatcher); - - ArgumentCaptor> responseObserverCaptor = - ArgumentCaptor.forClass(null); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - StreamObserver responseObserver = - responseObserverCaptor.getValue(); // same as responseObservers.poll() - - // Management server sends back an LDS response telling client to do RDS. - Rds rdsConfig = - Rds.newBuilder() - // Must set to use ADS. - .setConfigSource( - ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) - .setRouteConfigName("route-foo.googleapis.com") - .build(); - - List listeners = ImmutableList.of( - Any.pack(buildListener(TARGET_AUTHORITY, /* matching resource */ - Any.pack(HttpConnectionManager.newBuilder().setRds(rdsConfig).build()))) - ); - DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000"); - responseObserver.onNext(response); - - // Client sent an RDS request for resource "route-foo.googleapis.com" (Omitted). - - ScheduledTask rdsRespTimer = - Iterables.getOnlyElement( - fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); - assertThat(rdsRespTimer.isCancelled()).isFalse(); - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC - 1, TimeUnit.SECONDS); - - // RPC stream is broken while the initial fetch for the resource is not complete. - responseObserver.onError(Status.UNAVAILABLE.asException()); - assertThat(rdsRespTimer.isCancelled()).isTrue(); - - // Reset backoff and retry immediately. - inOrder.verify(backoffPolicyProvider).get(); - fakeClock.runDueTasks(); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - responseObserver = responseObserverCaptor.getValue(); - StreamObserver requestObserver = requestObservers.poll(); - - ScheduledTask ldsRespTimer = - Iterables.getOnlyElement( - fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); - assertThat(ldsRespTimer.getDelay(TimeUnit.SECONDS)) - .isEqualTo(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC); - - // Client resumed requests and management server sends back LDS resources again. - verify(requestObserver).onNext( - eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS, ""))); - responseObserver.onNext(response); - - // Client sent an RDS request for resource "route-foo.googleapis.com" (Omitted). - - assertThat(ldsRespTimer.isCancelled()).isTrue(); - rdsRespTimer = - Iterables.getOnlyElement( - fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); - assertThat(rdsRespTimer.getDelay(TimeUnit.SECONDS)) - .isEqualTo(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC); - - // Management server sends back an RDS response containing the RouteConfiguration - // for the requested resource. - List routeConfigs = ImmutableList.of( - Any.pack( - buildRouteConfiguration( - "route-foo.googleapis.com", // target route configuration - ImmutableList.of( - buildVirtualHost( - ImmutableList.of(TARGET_AUTHORITY), // matching virtual host - "cluster-foo.googleapis.com"))))); - response = buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0000"); - responseObserver.onNext(response); - - assertThat(rdsRespTimer.isCancelled()).isTrue(); - - // Resets RPC stream again. - responseObserver.onError(Status.UNAVAILABLE.asException()); - // Reset backoff and retry immediately. - inOrder.verify(backoffPolicyProvider).get(); - fakeClock.runDueTasks(); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - responseObserver = responseObserverCaptor.getValue(); - - // Client/server resumed LDS/RDS request/response (Omitted). - - // Start watching cluster data. - xdsClient.watchCdsResource("cluster-foo.googleapis.com", cdsResourceWatcher); - ScheduledTask cdsRespTimeoutTask = - Iterables.getOnlyElement( - fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); - assertThat(cdsRespTimeoutTask.isCancelled()).isFalse(); - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC - 1, TimeUnit.SECONDS); - - // RPC stream is broken while the initial fetch for the resource is not complete. - responseObserver.onError(Status.UNAVAILABLE.asException()); - assertThat(cdsRespTimeoutTask.isCancelled()).isTrue(); - inOrder.verify(backoffPolicy2).nextBackoffNanos(); - assertThat(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)).hasSize(1); - - // Retry after backoff. - fakeClock.forwardNanos(20L); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - responseObserver = responseObserverCaptor.getValue(); - - // Timer is rescheduled as the client restarts the resource fetch. - cdsRespTimeoutTask = - Iterables.getOnlyElement( - fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); - assertThat(cdsRespTimeoutTask.isCancelled()).isFalse(); - assertThat(cdsRespTimeoutTask.getDelay(TimeUnit.SECONDS)) - .isEqualTo(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC); - - // Start watching endpoint data. - xdsClient.watchEdsResource("cluster-foo.googleapis.com", edsResourceWatcher); - ScheduledTask edsTimeoutTask = - Iterables.getOnlyElement( - fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); - assertThat(edsTimeoutTask.getDelay(TimeUnit.SECONDS)) - .isEqualTo(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC); - - // RPC stream is broken again. - responseObserver.onError(Status.UNAVAILABLE.asException()); - - assertThat(edsTimeoutTask.isCancelled()).isTrue(); - inOrder.verify(backoffPolicy2).nextBackoffNanos(); - assertThat(fakeClock.getPendingTasks(RPC_RETRY_TASK_FILTER)).hasSize(1); - - fakeClock.forwardNanos(200L); - inOrder.verify(mockedDiscoveryService) - .streamAggregatedResources(responseObserverCaptor.capture()); - - assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); - } - - /** - * Tests sending a streaming LRS RPC for each cluster to report loads for. - */ - @Test - public void reportLoadStatsToServer() { - String clusterName = "cluster-foo.googleapis.com"; - xdsClient.addClientStats(clusterName, null); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(null); - xdsClient.reportClientStats(); - LoadReportCall lrsCall = loadReportCalls.poll(); - verify(lrsCall.requestObserver).onNext(requestCaptor.capture()); - assertThat(requestCaptor.getValue().getClusterStatsCount()) - .isEqualTo(0); // initial request - - lrsCall.responseObserver.onNext( - LoadStatsResponse.newBuilder() - .addClusters(clusterName) - .setLoadReportingInterval(Durations.fromNanos(1000L)) - .build()); - fakeClock.forwardNanos(1000L); - verify(lrsCall.requestObserver, times(2)).onNext(requestCaptor.capture()); - ClusterStats report = Iterables.getOnlyElement(requestCaptor.getValue().getClusterStatsList()); - assertThat(report.getClusterName()).isEqualTo(clusterName); - - xdsClient.removeClientStats(clusterName, null); - fakeClock.forwardNanos(1000L); - verify(lrsCall.requestObserver, times(3)).onNext(requestCaptor.capture()); - assertThat(requestCaptor.getValue().getClusterStatsCount()) - .isEqualTo(0); // no more stats reported - - xdsClient.cancelClientStatsReport(); - assertThat(lrsEnded.get()).isTrue(); - // See more test on LoadReportClientTest.java - } - - // Simulates the use case of watching clusters/endpoints based on service config resolved by - // LDS/RDS. - private void waitUntilConfigResolved(StreamObserver responseObserver) { - // Client sent an LDS request for resource TARGET_AUTHORITY (Omitted). - - // Management server responses with a listener telling client to do RDS. - Rds rdsConfig = - Rds.newBuilder() - .setConfigSource( - ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance())) - .setRouteConfigName("route-foo.googleapis.com") - .build(); - - List listeners = ImmutableList.of( - Any.pack(buildListener(TARGET_AUTHORITY, /* matching resource */ - Any.pack(HttpConnectionManager.newBuilder().setRds(rdsConfig).build()))) - ); - DiscoveryResponse ldsResponse = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000"); - responseObserver.onNext(ldsResponse); - - // Client sent an LDS ACK request and an RDS request for resource - // "route-foo.googleapis.com" (Omitted). - - // Management server sends an RDS response. - List routeConfigs = ImmutableList.of( - Any.pack( - buildRouteConfiguration( - "route-foo.googleapis.com", // target route configuration - ImmutableList.of( - buildVirtualHost( - ImmutableList.of(TARGET_AUTHORITY), // matching virtual host - "cluster.googleapis.com"))))); - DiscoveryResponse rdsResponse = - buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0000"); - responseObserver.onNext(rdsResponse); - } - - @Test - public void matchHostName_exactlyMatch() { - String pattern = "foo.googleapis.com"; - assertThat(XdsClientImpl.matchHostName("bar.googleapis.com", pattern)).isFalse(); - assertThat(XdsClientImpl.matchHostName("fo.googleapis.com", pattern)).isFalse(); - assertThat(XdsClientImpl.matchHostName("oo.googleapis.com", pattern)).isFalse(); - assertThat(XdsClientImpl.matchHostName("googleapis.com", pattern)).isFalse(); - assertThat(XdsClientImpl.matchHostName("foo.googleapis", pattern)).isFalse(); - assertThat(XdsClientImpl.matchHostName("foo.googleapis.com", pattern)).isTrue(); - } - - @Test - public void matchHostName_prefixWildcard() { - String pattern = "*.foo.googleapis.com"; - assertThat(XdsClientImpl.matchHostName("foo.googleapis.com", pattern)).isFalse(); - assertThat(XdsClientImpl.matchHostName("bar-baz.foo.googleapis", pattern)).isFalse(); - assertThat(XdsClientImpl.matchHostName("bar.foo.googleapis.com", pattern)).isTrue(); - pattern = "*-bar.foo.googleapis.com"; - assertThat(XdsClientImpl.matchHostName("bar.foo.googleapis.com", pattern)).isFalse(); - assertThat(XdsClientImpl.matchHostName("baz-bar.foo.googleapis", pattern)).isFalse(); - assertThat(XdsClientImpl.matchHostName("-bar.foo.googleapis.com", pattern)).isFalse(); - assertThat(XdsClientImpl.matchHostName("baz-bar.foo.googleapis.com", pattern)) - .isTrue(); - } - - @Test - public void matchHostName_postfixWildCard() { - String pattern = "foo.*"; - assertThat(XdsClientImpl.matchHostName("bar.googleapis.com", pattern)).isFalse(); - assertThat(XdsClientImpl.matchHostName("bar.foo.googleapis.com", pattern)).isFalse(); - assertThat(XdsClientImpl.matchHostName("foo.googleapis.com", pattern)).isTrue(); - assertThat(XdsClientImpl.matchHostName("foo.com", pattern)).isTrue(); - pattern = "foo-*"; - assertThat(XdsClientImpl.matchHostName("bar-.googleapis.com", pattern)).isFalse(); - assertThat(XdsClientImpl.matchHostName("foo.googleapis.com", pattern)).isFalse(); - assertThat(XdsClientImpl.matchHostName("foo.googleapis.com", pattern)).isFalse(); - assertThat(XdsClientImpl.matchHostName("foo-", pattern)).isFalse(); - assertThat(XdsClientImpl.matchHostName("foo-bar.com", pattern)).isTrue(); - assertThat(XdsClientImpl.matchHostName("foo-.com", pattern)).isTrue(); - assertThat(XdsClientImpl.matchHostName("foo-bar", pattern)).isTrue(); - } - - @Test - public void findVirtualHostForHostName_exactMatchFirst() { - String hostname = "a.googleapis.com"; - VirtualHost vHost1 = - VirtualHost.newBuilder() - .setName("virtualhost01.googleapis.com") // don't care - .addAllDomains(ImmutableList.of("a.googleapis.com", "b.googleapis.com")) - .build(); - VirtualHost vHost2 = - VirtualHost.newBuilder() - .setName("virtualhost02.googleapis.com") // don't care - .addAllDomains(ImmutableList.of("*.googleapis.com")) - .build(); - VirtualHost vHost3 = - VirtualHost.newBuilder() - .setName("virtualhost03.googleapis.com") // don't care - .addAllDomains(ImmutableList.of("*")) - .build(); - RouteConfiguration routeConfig = - RouteConfiguration.newBuilder() - .setName("route-foo.googleapis.com") - .addAllVirtualHosts(ImmutableList.of(vHost1, vHost2, vHost3)) - .build(); - assertThat(XdsClientImpl.findVirtualHostForHostName(routeConfig, hostname)).isEqualTo(vHost1); - } - - @Test - public void findVirtualHostForHostName_preferSuffixDomainOverPrefixDomain() { - String hostname = "a.googleapis.com"; - VirtualHost vHost1 = - VirtualHost.newBuilder() - .setName("virtualhost01.googleapis.com") // don't care - .addAllDomains(ImmutableList.of("*.googleapis.com", "b.googleapis.com")) - .build(); - VirtualHost vHost2 = - VirtualHost.newBuilder() - .setName("virtualhost02.googleapis.com") // don't care - .addAllDomains(ImmutableList.of("a.googleapis.*")) - .build(); - VirtualHost vHost3 = - VirtualHost.newBuilder() - .setName("virtualhost03.googleapis.com") // don't care - .addAllDomains(ImmutableList.of("*")) - .build(); - RouteConfiguration routeConfig = - RouteConfiguration.newBuilder() - .setName("route-foo.googleapis.com") - .addAllVirtualHosts(ImmutableList.of(vHost1, vHost2, vHost3)) - .build(); - assertThat(XdsClientImpl.findVirtualHostForHostName(routeConfig, hostname)).isEqualTo(vHost1); - } - - @Test - public void findVirtualHostForHostName_asteriskMatchAnyDomain() { - String hostname = "a.googleapis.com"; - VirtualHost vHost1 = - VirtualHost.newBuilder() - .setName("virtualhost01.googleapis.com") // don't care - .addAllDomains(ImmutableList.of("*")) - .build(); - VirtualHost vHost2 = - VirtualHost.newBuilder() - .setName("virtualhost02.googleapis.com") // don't care - .addAllDomains(ImmutableList.of("b.googleapis.com")) - .build(); - RouteConfiguration routeConfig = - RouteConfiguration.newBuilder() - .setName("route-foo.googleapis.com") - .addAllVirtualHosts(ImmutableList.of(vHost1, vHost2)) - .build(); - assertThat(XdsClientImpl.findVirtualHostForHostName(routeConfig, hostname)).isEqualTo(vHost1); - } - - @Test - public void populateRoutesInVirtualHost_routeWithCaseInsensitiveMatch() { - VirtualHost virtualHost = - VirtualHost.newBuilder() - .setName("virtualhost00.googleapis.com") // don't care - .addDomains(TARGET_AUTHORITY) - .addRoutes( - Route.newBuilder() - .setRoute(RouteAction.newBuilder().setCluster("cluster.googleapis.com")) - .setMatch( - RouteMatch.newBuilder() - .setPrefix("") - .setCaseSensitive(BoolValue.newBuilder().setValue(false)))) - .build(); - - thrown.expect(XdsClientImpl.InvalidProtoDataException.class); - XdsClientImpl.populateRoutesInVirtualHost(virtualHost); - } - - @Test - public void populateRoutesInVirtualHost_NoUsableRoute() { - VirtualHost virtualHost = - VirtualHost.newBuilder() - .setName("virtualhost00.googleapis.com") // don't care - .addDomains(TARGET_AUTHORITY) - .addRoutes( - // route with unsupported action - Route.newBuilder() - .setRoute(RouteAction.newBuilder().setClusterHeader("cluster header string")) - .setMatch(RouteMatch.newBuilder().setPrefix("/"))) - .addRoutes( - // route with unsupported matcher type - Route.newBuilder() - .setRoute(RouteAction.newBuilder().setCluster("cluster.googleapis.com")) - .setMatch( - RouteMatch.newBuilder() - .setPrefix("/") - .addQueryParameters(QueryParameterMatcher.getDefaultInstance()))) - .build(); - - thrown.expect(XdsClientImpl.InvalidProtoDataException.class); - XdsClientImpl.populateRoutesInVirtualHost(virtualHost); - } - - @Test - public void messagePrinter_printLdsResponse() { - MessagePrinter printer = new MessagePrinter(); - List listeners = ImmutableList.of( - Any.pack(buildListener("foo.googleapis.com:8080", - Any.pack( - HttpConnectionManager.newBuilder() - .setRouteConfig( - buildRouteConfiguration("route-foo.googleapis.com", - ImmutableList.of( - buildVirtualHost( - ImmutableList.of("foo.googleapis.com", "bar.googleapis.com"), - "cluster.googleapis.com")))) - .build())))); - DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS, "0000"); - - String expectedString = "{\n" - + " \"versionInfo\": \"0\",\n" - + " \"resources\": [{\n" - + " \"@type\": \"type.googleapis.com/envoy.config.listener.v3.Listener\",\n" - + " \"name\": \"foo.googleapis.com:8080\",\n" - + " \"address\": {\n" - + " },\n" - + " \"filterChains\": [{\n" - + " }],\n" - + " \"apiListener\": {\n" - + " \"apiListener\": {\n" - + " \"@type\": \"type.googleapis.com/envoy.extensions.filters.network" - + ".http_connection_manager.v3.HttpConnectionManager\",\n" - + " \"routeConfig\": {\n" - + " \"name\": \"route-foo.googleapis.com\",\n" - + " \"virtualHosts\": [{\n" - + " \"name\": \"virtualhost00.googleapis.com\",\n" - + " \"domains\": [\"foo.googleapis.com\", \"bar.googleapis.com\"],\n" - + " \"routes\": [{\n" - + " \"match\": {\n" - + " \"prefix\": \"\"\n" - + " },\n" - + " \"route\": {\n" - + " \"cluster\": \"cluster.googleapis.com\"\n" - + " }\n" - + " }]\n" - + " }]\n" - + " }\n" - + " }\n" - + " }\n" - + " }],\n" - + " \"typeUrl\": \"type.googleapis.com/envoy.config.listener.v3.Listener\",\n" - + " \"nonce\": \"0000\"\n" - + "}"; - String res = printer.print(response); - assertThat(res).isEqualTo(expectedString); - } - - @Test - public void messagePrinter_printRdsResponse() { - MessagePrinter printer = new MessagePrinter(); - List routeConfigs = - ImmutableList.of( - Any.pack( - buildRouteConfiguration( - "route-foo.googleapis.com", - ImmutableList.of( - buildVirtualHost( - ImmutableList.of("foo.googleapis.com", "bar.googleapis.com"), - "cluster.googleapis.com"))))); - DiscoveryResponse response = - buildDiscoveryResponse("213", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS, "0052"); - - String expectedString = "{\n" - + " \"versionInfo\": \"213\",\n" - + " \"resources\": [{\n" - + " \"@type\": \"type.googleapis.com/envoy.config.route.v3.RouteConfiguration\",\n" - + " \"name\": \"route-foo.googleapis.com\",\n" - + " \"virtualHosts\": [{\n" - + " \"name\": \"virtualhost00.googleapis.com\",\n" - + " \"domains\": [\"foo.googleapis.com\", \"bar.googleapis.com\"],\n" - + " \"routes\": [{\n" - + " \"match\": {\n" - + " \"prefix\": \"\"\n" - + " },\n" - + " \"route\": {\n" - + " \"cluster\": \"cluster.googleapis.com\"\n" - + " }\n" - + " }]\n" - + " }]\n" - + " }],\n" - + " \"typeUrl\": \"type.googleapis.com/envoy.config.route.v3.RouteConfiguration\",\n" - + " \"nonce\": \"0052\"\n" - + "}"; - String res = printer.print(response); - assertThat(res).isEqualTo(expectedString); - } - - @Test - public void messagePrinter_printCdsResponse() { - MessagePrinter printer = new MessagePrinter(); - List clusters = ImmutableList.of( - Any.pack(buildCluster("cluster-bar.googleapis.com", "service-blaze:cluster-bar", true)), - Any.pack(buildCluster("cluster-foo.googleapis.com", null, false))); - DiscoveryResponse response = - buildDiscoveryResponse("14", clusters, XdsClientImpl.ADS_TYPE_URL_CDS, "8"); - - String expectedString = "{\n" - + " \"versionInfo\": \"14\",\n" - + " \"resources\": [{\n" - + " \"@type\": \"type.googleapis.com/envoy.config.cluster.v3.Cluster\",\n" - + " \"name\": \"cluster-bar.googleapis.com\",\n" - + " \"type\": \"EDS\",\n" - + " \"edsClusterConfig\": {\n" - + " \"edsConfig\": {\n" - + " \"ads\": {\n" - + " }\n" - + " },\n" - + " \"serviceName\": \"service-blaze:cluster-bar\"\n" - + " },\n" - + " \"lrsServer\": {\n" - + " \"self\": {\n" - + " }\n" - + " }\n" - + " }, {\n" - + " \"@type\": \"type.googleapis.com/envoy.config.cluster.v3.Cluster\",\n" - + " \"name\": \"cluster-foo.googleapis.com\",\n" - + " \"type\": \"EDS\",\n" - + " \"edsClusterConfig\": {\n" - + " \"edsConfig\": {\n" - + " \"ads\": {\n" - + " }\n" - + " }\n" - + " }\n" - + " }],\n" - + " \"typeUrl\": \"type.googleapis.com/envoy.config.cluster.v3.Cluster\",\n" - + " \"nonce\": \"8\"\n" - + "}"; - String res = printer.print(response); - assertThat(res).isEqualTo(expectedString); - } - - @Test - public void messagePrinter_printEdsResponse() { - MessagePrinter printer = new MessagePrinter(); - List clusterLoadAssignments = ImmutableList.of( - Any.pack(buildClusterLoadAssignment("cluster-foo.googleapis.com", - ImmutableList.of( - buildLocalityLbEndpoints("region1", "zone1", "subzone1", - ImmutableList.of( - buildLbEndpoint("192.168.0.1", 8080, HealthStatus.HEALTHY, 2)), - 1, 0), - buildLocalityLbEndpoints("region3", "zone3", "subzone3", - ImmutableList.of( - buildLbEndpoint("192.168.142.5", 80, HealthStatus.UNHEALTHY, 5)), - 2, 1)), - ImmutableList.of( - buildDropOverload("lb", 200), - buildDropOverload("throttle", 1000))))); - - DiscoveryResponse response = - buildDiscoveryResponse("5", clusterLoadAssignments, - XdsClientImpl.ADS_TYPE_URL_EDS, "004"); - - String expectedString = "{\n" - + " \"versionInfo\": \"5\",\n" - + " \"resources\": [{\n" - + " \"@type\": \"type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment\",\n" - + " \"clusterName\": \"cluster-foo.googleapis.com\",\n" - + " \"endpoints\": [{\n" - + " \"locality\": {\n" - + " \"region\": \"region1\",\n" - + " \"zone\": \"zone1\",\n" - + " \"subZone\": \"subzone1\"\n" - + " },\n" - + " \"lbEndpoints\": [{\n" - + " \"endpoint\": {\n" - + " \"address\": {\n" - + " \"socketAddress\": {\n" - + " \"address\": \"192.168.0.1\",\n" - + " \"portValue\": 8080\n" - + " }\n" - + " }\n" - + " },\n" - + " \"healthStatus\": \"HEALTHY\",\n" - + " \"loadBalancingWeight\": 2\n" - + " }],\n" - + " \"loadBalancingWeight\": 1\n" - + " }, {\n" - + " \"locality\": {\n" - + " \"region\": \"region3\",\n" - + " \"zone\": \"zone3\",\n" - + " \"subZone\": \"subzone3\"\n" - + " },\n" - + " \"lbEndpoints\": [{\n" - + " \"endpoint\": {\n" - + " \"address\": {\n" - + " \"socketAddress\": {\n" - + " \"address\": \"192.168.142.5\",\n" - + " \"portValue\": 80\n" - + " }\n" - + " }\n" - + " },\n" - + " \"healthStatus\": \"UNHEALTHY\",\n" - + " \"loadBalancingWeight\": 5\n" - + " }],\n" - + " \"loadBalancingWeight\": 2,\n" - + " \"priority\": 1\n" - + " }],\n" - + " \"policy\": {\n" - + " \"dropOverloads\": [{\n" - + " \"category\": \"lb\",\n" - + " \"dropPercentage\": {\n" - + " \"numerator\": 200,\n" - + " \"denominator\": \"MILLION\"\n" - + " }\n" - + " }, {\n" - + " \"category\": \"throttle\",\n" - + " \"dropPercentage\": {\n" - + " \"numerator\": 1000,\n" - + " \"denominator\": \"MILLION\"\n" - + " }\n" - + " }]\n" - + " }\n" - + " }],\n" - + " \"typeUrl\": \"type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment\",\n" - + " \"nonce\": \"004\"\n" - + "}"; - String res = printer.print(response); - assertThat(res).isEqualTo(expectedString); - } - - private static void assertConfigUpdateContainsSingleClusterRoute( - ConfigUpdate configUpdate, String expectedClusterName) { - List routes = configUpdate.getRoutes(); - assertThat(routes).hasSize(1); - assertThat(Iterables.getOnlyElement(routes).getRouteAction().getCluster()) - .isEqualTo(expectedClusterName); - } - - /** - * Matcher for DiscoveryRequest without the comparison of error_details field, which is used for - * management server debugging purposes. - * - *

In general, if you are sure error_details field should not be set in a DiscoveryRequest, - * compare with message equality. Otherwise, this matcher is handy for comparing other fields - * only. - */ - private static class DiscoveryRequestMatcher implements ArgumentMatcher { - private final String versionInfo; - private final String typeUrl; - private final Set resourceNames; - private final String responseNonce; - - private DiscoveryRequestMatcher(String versionInfo, String resourceName, String typeUrl, - String responseNonce) { - this(versionInfo, ImmutableList.of(resourceName), typeUrl, responseNonce); - } - - private DiscoveryRequestMatcher( - String versionInfo, List resourceNames, String typeUrl, String responseNonce) { - this.versionInfo = versionInfo; - this.resourceNames = new HashSet<>(resourceNames); - this.typeUrl = typeUrl; - this.responseNonce = responseNonce; - } - - @Override - public boolean matches(DiscoveryRequest argument) { - if (!typeUrl.equals(argument.getTypeUrl())) { - return false; - } - if (!versionInfo.equals(argument.getVersionInfo())) { - return false; - } - if (!responseNonce.equals(argument.getResponseNonce())) { - return false; - } - if (!resourceNames.equals(new HashSet<>(argument.getResourceNamesList()))) { - return false; - } - return argument.getNode().equals(NODE.toEnvoyProtoNode()); - } - } - - private static class LoadReportCall { - private final StreamObserver requestObserver; - @SuppressWarnings("unused") - private final StreamObserver responseObserver; - - LoadReportCall(StreamObserver requestObserver, - StreamObserver responseObserver) { - this.requestObserver = requestObserver; - this.responseObserver = responseObserver; - } - } -} diff --git a/xds/src/test/java/io/grpc/xds/XdsClientImplTestForListener.java b/xds/src/test/java/io/grpc/xds/XdsClientImplTestForListener.java index c4028eec34..2ecbf3f5a1 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientImplTestForListener.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientImplTestForListener.java @@ -66,10 +66,10 @@ import io.grpc.stub.StreamObserver; import io.grpc.testing.GrpcCleanupRule; import io.grpc.xds.EnvoyProtoData.Address; import io.grpc.xds.EnvoyProtoData.Node; -import io.grpc.xds.XdsClient.ConfigWatcher; import io.grpc.xds.XdsClient.ListenerUpdate; import io.grpc.xds.XdsClient.ListenerWatcher; import io.grpc.xds.XdsClient.XdsChannel; +import io.grpc.xds.XdsClientImpl2.ResourceType; import io.grpc.xds.internal.sds.CommonTlsContextTestsUtil; import java.io.IOException; import java.util.ArrayDeque; @@ -94,7 +94,7 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; /** - * Tests for {@link XdsClientImpl for server side Listeners}. + * Tests for {@link XdsClientImpl2 for server side Listeners}. */ @RunWith(JUnit4.class) public class XdsClientImplTestForListener { @@ -111,7 +111,7 @@ public class XdsClientImplTestForListener { new FakeClock.TaskFilter() { @Override public boolean shouldAccept(Runnable command) { - return command.toString().contains(XdsClientImpl.RpcRetryTask.class.getSimpleName()); + return command.toString().contains(XdsClientImpl2.RpcRetryTask.class.getSimpleName()); } }; private static final TaskFilter LISTENER_RESOURCE_FETCH_TIMEOUT_TASK_FILTER = @@ -119,7 +119,7 @@ public class XdsClientImplTestForListener { @Override public boolean shouldAccept(Runnable command) { return command.toString() - .contains(XdsClientImpl.ListenerResourceFetchTimeoutTask.class.getSimpleName()); + .contains(XdsClientImpl2.ListenerResourceFetchTimeoutTask.class.getSimpleName()); } }; private static final String LISTENER_NAME = "INBOUND_LISTENER"; @@ -149,12 +149,10 @@ public class XdsClientImplTestForListener { @Mock private BackoffPolicy backoffPolicy2; @Mock - private ConfigWatcher configWatcher; - @Mock private ListenerWatcher listenerWatcher; private ManagedChannel channel; - private XdsClientImpl xdsClient; + private XdsClientImpl2 xdsClient; @Before public void setUp() throws IOException { @@ -198,7 +196,7 @@ public class XdsClientImplTestForListener { cleanupRule.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()); xdsClient = - new XdsClientImpl("", new XdsChannel(channel, /* useProtocolV3= */ false), NODE, + new XdsClientImpl2("", new XdsChannel(channel, /* useProtocolV3= */ false), NODE, syncContext, fakeClock.getScheduledExecutorService(), backoffPolicyProvider, fakeClock.getStopwatchSupplier()); // Only the connection to management server is established, no RPC request is sent until at @@ -238,34 +236,6 @@ public class XdsClientImplTestForListener { .build(); } - /** Error when ConfigWatcher and then ListenerWatcher registered. */ - @Test - public void ldsResponse_configAndListenerWatcher_expectError() { - xdsClient.watchConfigData("somehost:80", configWatcher); - try { - xdsClient.watchListenerData(PORT, listenerWatcher); - fail("expected exception"); - } catch (IllegalStateException expected) { - assertThat(expected) - .hasMessageThat() - .isEqualTo("ListenerWatcher cannot be set when ConfigWatcher set"); - } - } - - /** Error when ListenerWatcher and then ConfigWatcher registered. */ - @Test - public void ldsResponse_listenerAndConfigWatcher_expectError() { - xdsClient.watchListenerData(PORT, listenerWatcher); - try { - xdsClient.watchConfigData("somehost:80", configWatcher); - fail("expected exception"); - } catch (IllegalStateException expected) { - assertThat(expected) - .hasMessageThat() - .isEqualTo("ListenerWatcher already registered"); - } - } - /** Error when 2 ListenerWatchers registered. */ @Test public void ldsResponse_2listenerWatchers_expectError() { @@ -292,7 +262,7 @@ public class XdsClientImplTestForListener { // Client sends an LDS request with null in lds resource name verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + ResourceType.LDS.typeUrlV2(), ""))); assertThat(fakeClock.getPendingTasks(LISTENER_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); List listeners = ImmutableList.of( @@ -315,18 +285,18 @@ public class XdsClientImplTestForListener { "cluster-baz.googleapis.com")))) .build())))); DiscoveryResponse response = - buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + buildDiscoveryResponseV2("0", listeners, ResourceType.LDS.typeUrlV2(), "0000"); responseObserver.onNext(response); // Client sends an ACK LDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "0", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + ResourceType.LDS.typeUrlV2(), "0000"))); verify(listenerWatcher, never()).onListenerChanged(any(ListenerUpdate.class)); verify(listenerWatcher, never()).onResourceDoesNotExist(":" + PORT); verify(listenerWatcher, never()).onError(any(Status.class)); - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); + fakeClock.forwardTime(XdsClientImpl2.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); verify(listenerWatcher).onResourceDoesNotExist(":" + PORT); assertThat(fakeClock.getPendingTasks(LISTENER_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); } @@ -341,7 +311,7 @@ public class XdsClientImplTestForListener { // Client sends an LDS request with null in lds resource name verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + ResourceType.LDS.typeUrlV2(), ""))); assertThat(fakeClock.getPendingTasks(LISTENER_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); final FilterChain filterChainOutbound = buildFilterChain(buildFilterChainMatch(8000), null); @@ -367,18 +337,18 @@ public class XdsClientImplTestForListener { filterChainInbound ))); DiscoveryResponse response = - buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + buildDiscoveryResponseV2("0", listeners, ResourceType.LDS.typeUrlV2(), "0000"); responseObserver.onNext(response); // Client sends an ACK LDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "0", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + ResourceType.LDS.typeUrlV2(), "0000"))); verify(listenerWatcher, never()).onListenerChanged(any(ListenerUpdate.class)); verify(listenerWatcher, never()).onResourceDoesNotExist(":" + PORT); verify(listenerWatcher, never()).onError(any(Status.class)); - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); + fakeClock.forwardTime(XdsClientImpl2.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); verify(listenerWatcher).onResourceDoesNotExist(":" + PORT); assertThat(fakeClock.getPendingTasks(LISTENER_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); } @@ -393,7 +363,7 @@ public class XdsClientImplTestForListener { // Client sends an LDS request with null in lds resource name verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + ResourceType.LDS.typeUrlV2(), ""))); assertThat(fakeClock.getPendingTasks(LISTENER_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); final FilterChain filterChainOutbound = buildFilterChain(buildFilterChainMatch(8000), null); @@ -419,13 +389,13 @@ public class XdsClientImplTestForListener { filterChainInbound ))); DiscoveryResponse response = - buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + buildDiscoveryResponseV2("0", listeners, ResourceType.LDS.typeUrlV2(), "0000"); responseObserver.onNext(response); // Client sends an ACK LDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "0", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + ResourceType.LDS.typeUrlV2(), "0000"))); ArgumentCaptor listenerUpdateCaptor = ArgumentCaptor.forClass(null); verify(listenerWatcher, times(1)).onListenerChanged(listenerUpdateCaptor.capture()); @@ -468,7 +438,7 @@ public class XdsClientImplTestForListener { // Client sends an LDS request with null in lds resource name verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + ResourceType.LDS.typeUrlV2(), ""))); assertThat(fakeClock.getPendingTasks(LISTENER_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); final FilterChain filterChainOutbound = buildFilterChain(buildFilterChainMatch(8000), null); @@ -494,13 +464,13 @@ public class XdsClientImplTestForListener { filterChainInbound ))); DiscoveryResponse response = - buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + buildDiscoveryResponseV2("0", listeners, ResourceType.LDS.typeUrlV2(), "0000"); responseObserver.onNext(response); // Client sends an ACK LDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "0", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + ResourceType.LDS.typeUrlV2(), "0000"))); ArgumentCaptor listenerUpdateCaptor = ArgumentCaptor.forClass(null); verify(listenerWatcher, times(1)).onListenerChanged(listenerUpdateCaptor.capture()); @@ -517,13 +487,13 @@ public class XdsClientImplTestForListener { filterChainNewInbound ))); DiscoveryResponse response1 = - buildDiscoveryResponseV2("1", listeners1, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0001"); + buildDiscoveryResponseV2("1", listeners1, ResourceType.LDS.typeUrlV2(), "0001"); responseObserver.onNext(response1); // Client sends an ACK LDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "1", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0001"))); + ResourceType.LDS.typeUrlV2(), "0001"))); // Updated listener is notified to config watcher. listenerUpdateCaptor = ArgumentCaptor.forClass(null); @@ -564,7 +534,7 @@ public class XdsClientImplTestForListener { // Client sends an LDS request with null in lds resource name verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + ResourceType.LDS.typeUrlV2(), ""))); assertThat(fakeClock.getPendingTasks(LISTENER_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); final FilterChain filterChainInbound = buildFilterChain(buildFilterChainMatch(8000), null); @@ -590,13 +560,13 @@ public class XdsClientImplTestForListener { filterChainOutbound ))); DiscoveryResponse response = - buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + buildDiscoveryResponseV2("0", listeners, ResourceType.LDS.typeUrlV2(), "0000"); responseObserver.onNext(response); // Client sends an ACK LDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "0", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + ResourceType.LDS.typeUrlV2(), "0000"))); verify(listenerWatcher, never()).onError(any(Status.class)); verify(listenerWatcher, never()).onListenerChanged(any(ListenerUpdate.class)); @@ -612,7 +582,7 @@ public class XdsClientImplTestForListener { // Client sends an LDS request with null in lds resource name verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + ResourceType.LDS.typeUrlV2(), ""))); assertThat(fakeClock.getPendingTasks(LISTENER_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); final FilterChain filterChainInbound = buildFilterChain(buildFilterChainMatch(8000), null); @@ -639,18 +609,18 @@ public class XdsClientImplTestForListener { filterChainOutbound ))); DiscoveryResponse response = - buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + buildDiscoveryResponseV2("0", listeners, ResourceType.LDS.typeUrlV2(), "0000"); responseObserver.onNext(response); // Client sends an ACK LDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "0", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + ResourceType.LDS.typeUrlV2(), "0000"))); verify(listenerWatcher, never()).onListenerChanged(any(ListenerUpdate.class)); verify(listenerWatcher, never()).onResourceDoesNotExist(":" + PORT); verify(listenerWatcher, never()).onError(any(Status.class)); - fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); + fakeClock.forwardTime(XdsClientImpl2.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); verify(listenerWatcher).onResourceDoesNotExist(":" + PORT); assertThat(fakeClock.getPendingTasks(LISTENER_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); } @@ -674,7 +644,7 @@ public class XdsClientImplTestForListener { StreamObserver requestObserver = requestObservers.poll(); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + ResourceType.LDS.typeUrlV2(), ""))); final FilterChain filterChainOutbound = buildFilterChain(buildFilterChainMatch(8000), null); final FilterChain filterChainInbound = buildFilterChain(buildFilterChainMatch(PORT, @@ -690,7 +660,7 @@ public class XdsClientImplTestForListener { filterChainInbound ))); DiscoveryResponse response = - buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + buildDiscoveryResponseV2("0", listeners, ResourceType.LDS.typeUrlV2(), "0000"); responseObserver.onNext(response); // Client sent an ACK CDS request (Omitted). @@ -713,7 +683,7 @@ public class XdsClientImplTestForListener { // Retry resumes requests for all wanted resources. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "0", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + ResourceType.LDS.typeUrlV2(), ""))); // Management server becomes unreachable. responseObserver.onError(Status.UNAVAILABLE.asException()); @@ -732,7 +702,7 @@ public class XdsClientImplTestForListener { requestObserver = requestObservers.poll(); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "0", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + ResourceType.LDS.typeUrlV2(), ""))); // Management server is still not reachable. responseObserver.onError(Status.UNAVAILABLE.asException()); @@ -751,11 +721,11 @@ public class XdsClientImplTestForListener { requestObserver = requestObservers.poll(); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "0", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + ResourceType.LDS.typeUrlV2(), ""))); // Management server sends back a LDS response. response = buildDiscoveryResponseV2("1", listeners, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0001"); + ResourceType.LDS.typeUrlV2(), "0001"); responseObserver.onNext(response); // Client sent an LDS ACK request (Omitted). @@ -774,7 +744,7 @@ public class XdsClientImplTestForListener { verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "1", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + ResourceType.LDS.typeUrlV2(), ""))); // Management server becomes unreachable again. responseObserver.onError(Status.UNAVAILABLE.asException()); @@ -792,7 +762,7 @@ public class XdsClientImplTestForListener { requestObserver = requestObservers.poll(); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(getNodeToVerify(), "1", - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + ResourceType.LDS.typeUrlV2(), ""))); verifyNoMoreInteractions(mockedDiscoveryService, backoffPolicyProvider, backoffPolicy1, backoffPolicy2);