grpc-java/xds/src/test/java/io/grpc/xds/XdsDependencyManagerTest.java

940 lines
40 KiB
Java

/*
* Copyright 2024 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.StatusMatcher.statusHasCode;
import static io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType.EDS;
import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_CDS;
import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_EDS;
import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_LDS;
import static io.grpc.xds.XdsTestControlPlaneService.ADS_TYPE_URL_RDS;
import static io.grpc.xds.XdsTestUtils.CLUSTER_NAME;
import static io.grpc.xds.XdsTestUtils.ENDPOINT_HOSTNAME;
import static io.grpc.xds.XdsTestUtils.ENDPOINT_PORT;
import static io.grpc.xds.XdsTestUtils.RDS_NAME;
import static io.grpc.xds.XdsTestUtils.getEdsNameForCluster;
import static io.grpc.xds.client.CommonBootstrapperTestUtils.SERVER_URI;
import static org.mockito.AdditionalAnswers.delegatesTo;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.atLeastOnce;
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 com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.protobuf.Message;
import io.envoyproxy.envoy.config.cluster.v3.Cluster;
import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment;
import io.envoyproxy.envoy.config.listener.v3.Listener;
import io.envoyproxy.envoy.config.route.v3.RouteConfiguration;
import io.grpc.BindableService;
import io.grpc.ChannelLogger;
import io.grpc.ManagedChannel;
import io.grpc.NameResolver;
import io.grpc.Server;
import io.grpc.Status;
import io.grpc.StatusOr;
import io.grpc.StatusOrMatcher;
import io.grpc.SynchronizationContext;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.internal.ExponentialBackoffPolicy;
import io.grpc.internal.FakeClock;
import io.grpc.internal.GrpcUtil;
import io.grpc.testing.GrpcCleanupRule;
import io.grpc.xds.XdsClusterResource.CdsUpdate;
import io.grpc.xds.XdsConfig.XdsClusterConfig;
import io.grpc.xds.XdsEndpointResource.EdsUpdate;
import io.grpc.xds.client.CommonBootstrapperTestUtils;
import io.grpc.xds.client.XdsClient;
import io.grpc.xds.client.XdsClientMetricReporter;
import io.grpc.xds.client.XdsTransportFactory;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.ArgumentMatchers;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
/** Unit tests for {@link XdsDependencyManager}. */
@RunWith(JUnit4.class)
public class XdsDependencyManagerTest {
private static final Logger log = Logger.getLogger(XdsDependencyManagerTest.class.getName());
public static final String CLUSTER_TYPE_NAME = XdsClusterResource.getInstance().typeName();
public static final String ENDPOINT_TYPE_NAME = XdsEndpointResource.getInstance().typeName();
@Mock
private XdsClientMetricReporter xdsClientMetricReporter;
private final SynchronizationContext syncContext =
new SynchronizationContext((t, e) -> {
throw new AssertionError(e);
});
private ManagedChannel channel;
private XdsClient xdsClient;
private XdsDependencyManager xdsDependencyManager;
private TestWatcher xdsConfigWatcher;
private Server xdsServer;
private final FakeClock fakeClock = new FakeClock();
private final String serverName = "the-service-name";
private final Queue<XdsTestUtils.LrsRpcCall> loadReportCalls = new ArrayDeque<>();
private final AtomicBoolean adsEnded = new AtomicBoolean(true);
private final AtomicBoolean lrsEnded = new AtomicBoolean(true);
private final XdsTestControlPlaneService controlPlaneService = new XdsTestControlPlaneService();
private final BindableService lrsService =
XdsTestUtils.createLrsService(lrsEnded, loadReportCalls);
@Rule
public final GrpcCleanupRule cleanupRule = new GrpcCleanupRule();
@Rule
public final MockitoRule mocks = MockitoJUnit.rule();
private TestWatcher testWatcher;
private XdsConfig defaultXdsConfig; // set in setUp()
@Captor
private ArgumentCaptor<StatusOr<XdsConfig>> xdsUpdateCaptor;
private final NameResolver.Args nameResolverArgs = NameResolver.Args.newBuilder()
.setDefaultPort(8080)
.setProxyDetector(GrpcUtil.DEFAULT_PROXY_DETECTOR)
.setSynchronizationContext(syncContext)
.setServiceConfigParser(mock(NameResolver.ServiceConfigParser.class))
.setChannelLogger(mock(ChannelLogger.class))
.setScheduledExecutorService(fakeClock.getScheduledExecutorService())
.build();
private final ScheduledExecutorService scheduler = fakeClock.getScheduledExecutorService();
@Before
public void setUp() throws Exception {
xdsServer = cleanupRule.register(InProcessServerBuilder
.forName("control-plane")
.addService(controlPlaneService)
.addService(lrsService)
.directExecutor()
.build()
.start());
XdsTestUtils.setAdsConfig(controlPlaneService, serverName);
channel = cleanupRule.register(
InProcessChannelBuilder.forName("control-plane").directExecutor().build());
XdsTransportFactory xdsTransportFactory =
ignore -> new GrpcXdsTransportFactory.GrpcXdsTransport(channel);
xdsClient = CommonBootstrapperTestUtils.createXdsClient(
Collections.singletonList(SERVER_URI), xdsTransportFactory, fakeClock,
new ExponentialBackoffPolicy.Provider(), MessagePrinter.INSTANCE, xdsClientMetricReporter);
testWatcher = new TestWatcher();
xdsConfigWatcher = mock(TestWatcher.class, delegatesTo(testWatcher));
defaultXdsConfig = XdsTestUtils.getDefaultXdsConfig(serverName);
}
@After
public void tearDown() throws InterruptedException {
if (xdsDependencyManager != null) {
xdsDependencyManager.shutdown();
}
xdsClient.shutdown();
channel.shutdown(); // channel not owned by XdsClient
xdsServer.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
assertThat(adsEnded.get()).isTrue();
assertThat(lrsEnded.get()).isTrue();
assertThat(fakeClock.getPendingTasks()).isEmpty();
}
@Test
public void verify_basic_config() {
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
verify(xdsConfigWatcher).onUpdate(StatusOr.fromValue(defaultXdsConfig));
testWatcher.verifyStats(1, 0);
}
@Test
public void verify_config_update() {
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
InOrder inOrder = Mockito.inOrder(xdsConfigWatcher);
inOrder.verify(xdsConfigWatcher).onUpdate(StatusOr.fromValue(defaultXdsConfig));
testWatcher.verifyStats(1, 0);
assertThat(testWatcher.lastConfig).isEqualTo(defaultXdsConfig);
XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS2", "CDS2", "EDS2",
ENDPOINT_HOSTNAME + "2", ENDPOINT_PORT + 2);
inOrder.verify(xdsConfigWatcher).onUpdate(ArgumentMatchers.notNull());
testWatcher.verifyStats(2, 0);
assertThat(testWatcher.lastConfig).isNotEqualTo(defaultXdsConfig);
}
@Test
public void verify_simple_aggregate() {
InOrder inOrder = Mockito.inOrder(xdsConfigWatcher);
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
inOrder.verify(xdsConfigWatcher).onUpdate(StatusOr.fromValue(defaultXdsConfig));
List<String> childNames = Arrays.asList("clusterC", "clusterB", "clusterA");
String rootName = "root_c";
RouteConfiguration routeConfig =
XdsTestUtils.buildRouteConfiguration(serverName, XdsTestUtils.RDS_NAME, rootName);
controlPlaneService.setXdsConfig(
ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, routeConfig));
XdsTestUtils.setAggregateCdsConfig(controlPlaneService, serverName, rootName, childNames);
inOrder.verify(xdsConfigWatcher).onUpdate(any());
Map<String, StatusOr<XdsClusterConfig>> lastConfigClusters =
testWatcher.lastConfig.getClusters();
assertThat(lastConfigClusters).hasSize(childNames.size() + 1);
StatusOr<XdsClusterConfig> rootC = lastConfigClusters.get(rootName);
assertThat(rootC.getValue().getChildren()).isInstanceOf(XdsClusterConfig.AggregateConfig.class);
XdsClusterConfig.AggregateConfig aggConfig =
(XdsClusterConfig.AggregateConfig) rootC.getValue().getChildren();
assertThat(aggConfig.getLeafNames()).isEqualTo(childNames);
for (String childName : childNames) {
assertThat(lastConfigClusters).containsKey(childName);
StatusOr<XdsClusterConfig> childConfigOr = lastConfigClusters.get(childName);
CdsUpdate childResource =
childConfigOr.getValue().getClusterResource();
assertThat(childResource.clusterType()).isEqualTo(EDS);
assertThat(childResource.edsServiceName()).isEqualTo(getEdsNameForCluster(childName));
StatusOr<EdsUpdate> endpoint = getEndpoint(childConfigOr);
assertThat(endpoint.hasValue()).isTrue();
assertThat(endpoint.getValue().clusterName).isEqualTo(getEdsNameForCluster(childName));
}
}
private static StatusOr<EdsUpdate> getEndpoint(StatusOr<XdsClusterConfig> childConfigOr) {
XdsClusterConfig.ClusterChild clusterChild = childConfigOr.getValue()
.getChildren();
assertThat(clusterChild).isInstanceOf(XdsClusterConfig.EndpointConfig.class);
StatusOr<EdsUpdate> endpoint = ((XdsClusterConfig.EndpointConfig) clusterChild).getEndpoint();
assertThat(endpoint).isNotNull();
return endpoint;
}
@Test
public void testComplexRegisteredAggregate() throws IOException {
InOrder inOrder = Mockito.inOrder(xdsConfigWatcher);
// Do initialization
String rootName1 = "root_c";
List<String> childNames = Arrays.asList("clusterC", "clusterB", "clusterA");
XdsTestUtils.addAggregateToExistingConfig(controlPlaneService, rootName1, childNames);
String rootName2 = "root_2";
List<String> childNames2 = Arrays.asList("clusterA", "clusterX");
XdsTestUtils.addAggregateToExistingConfig(controlPlaneService, rootName2, childNames2);
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
inOrder.verify(xdsConfigWatcher).onUpdate(any());
Closeable subscription1 = xdsDependencyManager.subscribeToCluster(rootName1);
inOrder.verify(xdsConfigWatcher).onUpdate(any());
Closeable subscription2 = xdsDependencyManager.subscribeToCluster(rootName2);
inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture());
testWatcher.verifyStats(3, 0);
ImmutableSet.Builder<String> builder = ImmutableSet.builder();
Set<String> expectedClusters = builder.add(rootName1).add(rootName2).add(CLUSTER_NAME)
.addAll(childNames).addAll(childNames2).build();
assertThat(xdsUpdateCaptor.getValue().getValue().getClusters().keySet())
.isEqualTo(expectedClusters);
// Close 1 subscription shouldn't affect the other or RDS subscriptions
subscription1.close();
inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture());
builder = ImmutableSet.builder();
Set<String> expectedClusters2 =
builder.add(rootName2).add(CLUSTER_NAME).addAll(childNames2).build();
assertThat(xdsUpdateCaptor.getValue().getValue().getClusters().keySet())
.isEqualTo(expectedClusters2);
subscription2.close();
inOrder.verify(xdsConfigWatcher).onUpdate(StatusOr.fromValue(defaultXdsConfig));
}
@Test
public void testDelayedSubscription() {
InOrder inOrder = Mockito.inOrder(xdsConfigWatcher);
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
inOrder.verify(xdsConfigWatcher).onUpdate(StatusOr.fromValue(defaultXdsConfig));
String rootName1 = "root_c";
Closeable subscription1 = xdsDependencyManager.subscribeToCluster(rootName1);
assertThat(subscription1).isNotNull();
fakeClock.forwardTime(16, TimeUnit.SECONDS);
inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture());
Status status = xdsUpdateCaptor.getValue().getValue().getClusters().get(rootName1).getStatus();
assertThat(status.getCode()).isEqualTo(Status.Code.UNAVAILABLE);
assertThat(status.getDescription()).contains(rootName1);
List<String> childNames = Arrays.asList("clusterC", "clusterB", "clusterA");
XdsTestUtils.addAggregateToExistingConfig(controlPlaneService, rootName1, childNames);
inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture());
assertThat(xdsUpdateCaptor.getValue().getValue().getClusters().get(rootName1).hasValue())
.isTrue();
}
@Test
public void testMissingCdsAndEds() {
// update config so that agg cluster references 2 existing & 1 non-existing cluster
List<String> childNames = Arrays.asList("clusterC", "clusterB", "clusterA");
Cluster cluster = XdsTestUtils.buildAggCluster(CLUSTER_NAME, childNames);
Map<String, Message> clusterMap = new HashMap<>();
Map<String, Message> edsMap = new HashMap<>();
clusterMap.put(CLUSTER_NAME, cluster);
for (int i = 0; i < childNames.size() - 1; i++) {
String edsName = XdsTestUtils.EDS_NAME + i;
Cluster child = ControlPlaneRule.buildCluster(childNames.get(i), edsName);
clusterMap.put(childNames.get(i), child);
}
controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap);
// Update config so that one of the 2 "valid" clusters has an EDS resource, the other does not
// and there is an EDS that doesn't have matching clusters
ClusterLoadAssignment clusterLoadAssignment = ControlPlaneRule.buildClusterLoadAssignment(
"127.0.1.1", ENDPOINT_HOSTNAME, ENDPOINT_PORT, XdsTestUtils.EDS_NAME + 0);
edsMap.put(XdsTestUtils.EDS_NAME + 0, clusterLoadAssignment);
clusterLoadAssignment = ControlPlaneRule.buildClusterLoadAssignment(
"127.0.1.2", ENDPOINT_HOSTNAME, ENDPOINT_PORT, "garbageEds");
edsMap.put("garbageEds", clusterLoadAssignment);
controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap);
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
fakeClock.forwardTime(16, TimeUnit.SECONDS);
verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture());
List<StatusOr<XdsClusterConfig>> returnedClusters = new ArrayList<>();
for (String childName : childNames) {
returnedClusters.add(xdsUpdateCaptor.getValue().getValue().getClusters().get(childName));
}
// Check that missing cluster reported Status and the other 2 are present
StatusOr<XdsClusterConfig> missingCluster = returnedClusters.get(2);
assertThat(missingCluster.getStatus().getCode()).isEqualTo(Status.Code.UNAVAILABLE);
assertThat(missingCluster.getStatus().getDescription()).contains(childNames.get(2));
assertThat(returnedClusters.get(0).hasValue()).isTrue();
assertThat(returnedClusters.get(1).hasValue()).isTrue();
// Check that missing EDS reported Status, the other one is present and the garbage EDS is not
assertThat(getEndpoint(returnedClusters.get(0)).hasValue()).isTrue();
assertThat(getEndpoint(returnedClusters.get(1)).getStatus().getCode())
.isEqualTo(Status.Code.UNAVAILABLE);
assertThat(getEndpoint(returnedClusters.get(1)).getStatus().getDescription())
.contains(XdsTestUtils.EDS_NAME + 1);
verify(xdsConfigWatcher, never()).onUpdate(
argThat(StatusOrMatcher.hasStatus(statusHasCode(Status.Code.UNAVAILABLE))));
testWatcher.verifyStats(1, 0);
}
@Test
public void testMissingLds() {
String ldsName = "badLdsName";
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, ldsName, nameResolverArgs, scheduler);
fakeClock.forwardTime(16, TimeUnit.SECONDS);
verify(xdsConfigWatcher).onUpdate(
argThat(StatusOrMatcher.hasStatus(statusHasCode(Status.Code.UNAVAILABLE)
.andDescriptionContains(ldsName))));
testWatcher.verifyStats(0, 1);
}
@Test
public void testTcpListenerErrors() {
Listener serverListener =
ControlPlaneRule.buildServerListener().toBuilder().setName(serverName).build();
controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS, ImmutableMap.of(serverName, serverListener));
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
fakeClock.forwardTime(16, TimeUnit.SECONDS);
verify(xdsConfigWatcher).onUpdate(
argThat(StatusOrMatcher.hasStatus(
statusHasCode(Status.Code.UNAVAILABLE).andDescriptionContains("Not an API listener"))));
testWatcher.verifyStats(0, 1);
}
@Test
public void testMissingRds() {
String rdsName = "badRdsName";
Listener clientListener = ControlPlaneRule.buildClientListener(serverName, rdsName);
controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS,
ImmutableMap.of(serverName, clientListener));
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
fakeClock.forwardTime(16, TimeUnit.SECONDS);
verify(xdsConfigWatcher).onUpdate(
argThat(StatusOrMatcher.hasStatus(statusHasCode(Status.Code.UNAVAILABLE)
.andDescriptionContains(rdsName))));
testWatcher.verifyStats(0, 1);
}
@Test
public void testUpdateToMissingVirtualHost() {
RouteConfiguration routeConfig = XdsTestUtils.buildRouteConfiguration(
"wrong-virtual-host", XdsTestUtils.RDS_NAME, XdsTestUtils.CLUSTER_NAME);
controlPlaneService.setXdsConfig(
ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, routeConfig));
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
// Update with a config that has a virtual host that doesn't match the server name
verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture());
assertThat(xdsUpdateCaptor.getValue().getStatus().getDescription())
.contains("Failed to find virtual host matching hostname: " + serverName);
testWatcher.verifyStats(0, 1);
}
@Test
public void testCorruptLds() {
String ldsResourceName =
"xdstp://unknown.example.com/envoy.config.listener.v3.Listener/listener1";
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, ldsResourceName, nameResolverArgs, scheduler);
verify(xdsConfigWatcher).onUpdate(
argThat(StatusOrMatcher.hasStatus(
statusHasCode(Status.Code.UNAVAILABLE).andDescriptionContains(ldsResourceName))));
fakeClock.forwardTime(16, TimeUnit.SECONDS);
testWatcher.verifyStats(0, 1);
}
@Test
public void testChangeRdsName_fromLds() {
InOrder inOrder = Mockito.inOrder(xdsConfigWatcher);
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
inOrder.verify(xdsConfigWatcher).onUpdate(StatusOr.fromValue(defaultXdsConfig));
String newRdsName = "newRdsName1";
Listener clientListener = buildInlineClientListener(newRdsName, CLUSTER_NAME);
controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS,
ImmutableMap.of(serverName, clientListener));
inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture());
assertThat(xdsUpdateCaptor.getValue().getValue()).isNotEqualTo(defaultXdsConfig);
assertThat(xdsUpdateCaptor.getValue().getValue().getVirtualHost().name()).isEqualTo(newRdsName);
}
@Test
public void testMultipleParentsInCdsTree() throws IOException {
/*
* Configure Xds server with the following cluster tree and point RDS to root:
2 aggregates under root A & B
B has EDS Cluster B1 && shared agg AB1; A has agg A1 && shared agg AB1
A1 has shared EDS Cluster A11 && shared agg AB1
AB1 has shared EDS Clusters A11 && AB11
As an alternate visualization, parents are:
A -> root, B -> root, A1 -> A, AB1 -> A|B|A1, B1 -> B, A11 -> A1|AB1, AB11 -> AB1
*/
Cluster rootCluster =
XdsTestUtils.buildAggCluster("root", Arrays.asList("clusterA", "clusterB"));
Cluster clusterA =
XdsTestUtils.buildAggCluster("clusterA", Arrays.asList("clusterA1", "clusterAB1"));
Cluster clusterB =
XdsTestUtils.buildAggCluster("clusterB", Arrays.asList("clusterB1", "clusterAB1"));
Cluster clusterA1 =
XdsTestUtils.buildAggCluster("clusterA1", Arrays.asList("clusterA11", "clusterAB1"));
Cluster clusterAB1 =
XdsTestUtils.buildAggCluster("clusterAB1", Arrays.asList("clusterA11", "clusterAB11"));
Map<String, Message> clusterMap = new HashMap<>();
Map<String, Message> edsMap = new HashMap<>();
clusterMap.put("root", rootCluster);
clusterMap.put("clusterA", clusterA);
clusterMap.put("clusterB", clusterB);
clusterMap.put("clusterA1", clusterA1);
clusterMap.put("clusterAB1", clusterAB1);
XdsTestUtils.addEdsClusters(clusterMap, edsMap, "clusterA11", "clusterAB11", "clusterB1");
RouteConfiguration routeConfig =
XdsTestUtils.buildRouteConfiguration(serverName, XdsTestUtils.RDS_NAME, "root");
controlPlaneService.setXdsConfig(
ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, routeConfig));
controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap);
controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap);
// Start the actual test
InOrder inOrder = Mockito.inOrder(xdsConfigWatcher);
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture());
XdsConfig initialConfig = xdsUpdateCaptor.getValue().getValue();
// Make sure that adding subscriptions that rds points at doesn't change the config
Closeable rootSub = xdsDependencyManager.subscribeToCluster("root");
assertThat(xdsDependencyManager.buildUpdate().getValue()).isEqualTo(initialConfig);
Closeable clusterAB11Sub = xdsDependencyManager.subscribeToCluster("clusterAB11");
assertThat(xdsDependencyManager.buildUpdate().getValue()).isEqualTo(initialConfig);
// Make sure that closing subscriptions that rds points at doesn't change the config
rootSub.close();
assertThat(xdsDependencyManager.buildUpdate().getValue()).isEqualTo(initialConfig);
clusterAB11Sub.close();
assertThat(xdsDependencyManager.buildUpdate().getValue()).isEqualTo(initialConfig);
// Make an explicit root subscription and then change RDS to point to A11
rootSub = xdsDependencyManager.subscribeToCluster("root");
RouteConfiguration newRouteConfig =
XdsTestUtils.buildRouteConfiguration(serverName, XdsTestUtils.RDS_NAME, "clusterA11");
controlPlaneService.setXdsConfig(
ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, newRouteConfig));
inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture());
assertThat(xdsUpdateCaptor.getValue().getValue().getClusters()).hasSize(8);
// Now that it is released, we should only have A11
rootSub.close();
inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture());
assertThat(xdsUpdateCaptor.getValue().getValue().getClusters().keySet())
.containsExactly("clusterA11");
}
@Test
public void testCdsCycle() throws Exception {
RouteConfiguration routeConfig =
XdsTestUtils.buildRouteConfiguration(serverName, XdsTestUtils.RDS_NAME, "clusterA");
Map<String, Message> clusterMap = new HashMap<>();
Map<String, Message> edsMap = new HashMap<>();
clusterMap.put("clusterA", XdsTestUtils.buildAggCluster("clusterA", Arrays.asList("clusterB")));
clusterMap.put("clusterB", XdsTestUtils.buildAggCluster("clusterB", Arrays.asList("clusterA")));
XdsTestUtils.addEdsClusters(clusterMap, edsMap, "clusterC");
controlPlaneService.setXdsConfig(
ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, routeConfig));
controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap);
controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap);
InOrder inOrder = Mockito.inOrder(xdsConfigWatcher);
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture());
XdsConfig config = xdsUpdateCaptor.getValue().getValue();
assertThat(config.getClusters().get("clusterA").hasValue()).isFalse();
assertThat(config.getClusters().get("clusterA").getStatus().getDescription()).contains("cycle");
}
@Test
public void testMultipleCdsReferToSameEds() {
// Create the maps and Update the config to have 2 clusters that refer to the same EDS resource
String edsName = "sharedEds";
Cluster rootCluster =
XdsTestUtils.buildAggCluster("root", Arrays.asList("clusterA", "clusterB"));
Cluster clusterA = ControlPlaneRule.buildCluster("clusterA", edsName);
Cluster clusterB = ControlPlaneRule.buildCluster("clusterB", edsName);
Map<String, Message> clusterMap = new HashMap<>();
clusterMap.put("root", rootCluster);
clusterMap.put("clusterA", clusterA);
clusterMap.put("clusterB", clusterB);
Map<String, Message> edsMap = new HashMap<>();
ClusterLoadAssignment clusterLoadAssignment = ControlPlaneRule.buildClusterLoadAssignment(
"127.0.1.4", ENDPOINT_HOSTNAME, ENDPOINT_PORT, edsName);
edsMap.put(edsName, clusterLoadAssignment);
RouteConfiguration routeConfig =
XdsTestUtils.buildRouteConfiguration(serverName, XdsTestUtils.RDS_NAME, "root");
controlPlaneService.setXdsConfig(
ADS_TYPE_URL_RDS, ImmutableMap.of(XdsTestUtils.RDS_NAME, routeConfig));
controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap);
controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap);
// Start the actual test
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture());
XdsConfig initialConfig = xdsUpdateCaptor.getValue().getValue();
assertThat(initialConfig.getClusters().keySet())
.containsExactly("root", "clusterA", "clusterB");
EdsUpdate edsForA = getEndpoint(initialConfig.getClusters().get("clusterA")).getValue();
assertThat(edsForA.clusterName).isEqualTo(edsName);
EdsUpdate edsForB = getEndpoint(initialConfig.getClusters().get("clusterB")).getValue();
assertThat(edsForB.clusterName).isEqualTo(edsName);
assertThat(edsForA).isEqualTo(edsForB);
edsForA.localityLbEndpointsMap.values().forEach(
localityLbEndpoints -> assertThat(localityLbEndpoints.endpoints()).hasSize(1));
}
@Test
public void testChangeRdsName_FromLds_complexTree() {
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
// Create the same tree as in testMultipleParentsInCdsTree
Cluster rootCluster =
XdsTestUtils.buildAggCluster("root", Arrays.asList("clusterA", "clusterB"));
Cluster clusterA =
XdsTestUtils.buildAggCluster("clusterA", Arrays.asList("clusterA1", "clusterAB1"));
Cluster clusterB =
XdsTestUtils.buildAggCluster("clusterB", Arrays.asList("clusterB1", "clusterAB1"));
Cluster clusterA1 =
XdsTestUtils.buildAggCluster("clusterA1", Arrays.asList("clusterA11", "clusterAB1"));
Cluster clusterAB1 =
XdsTestUtils.buildAggCluster("clusterAB1", Arrays.asList("clusterA11", "clusterAB11"));
Map<String, Message> clusterMap = new HashMap<>();
Map<String, Message> edsMap = new HashMap<>();
clusterMap.put("root", rootCluster);
clusterMap.put("clusterA", clusterA);
clusterMap.put("clusterB", clusterB);
clusterMap.put("clusterA1", clusterA1);
clusterMap.put("clusterAB1", clusterAB1);
XdsTestUtils.addEdsClusters(clusterMap, edsMap, "clusterA11", "clusterAB11", "clusterB1");
controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap);
controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap);
InOrder inOrder = Mockito.inOrder(xdsConfigWatcher);
inOrder.verify(xdsConfigWatcher, atLeastOnce()).onUpdate(any());
// Do the test
String newRdsName = "newRdsName1";
Listener clientListener = buildInlineClientListener(newRdsName, "root");
controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS,
ImmutableMap.of(serverName, clientListener));
inOrder.verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture());
XdsConfig config = xdsUpdateCaptor.getValue().getValue();
assertThat(config.getVirtualHost().name()).isEqualTo(newRdsName);
assertThat(config.getClusters()).hasSize(8);
}
@Test
public void testChangeAggCluster() {
InOrder inOrder = Mockito.inOrder(xdsConfigWatcher);
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
inOrder.verify(xdsConfigWatcher).onUpdate(any());
// Setup initial config A -> A1 -> (A11, A12)
Cluster rootCluster =
XdsTestUtils.buildAggCluster("root", Arrays.asList("clusterA"));
Cluster clusterA =
XdsTestUtils.buildAggCluster("clusterA", Arrays.asList("clusterA1"));
Cluster clusterA1 =
XdsTestUtils.buildAggCluster("clusterA1", Arrays.asList("clusterA11", "clusterA12"));
Map<String, Message> clusterMap = new HashMap<>();
Map<String, Message> edsMap = new HashMap<>();
clusterMap.put("root", rootCluster);
clusterMap.put("clusterA", clusterA);
clusterMap.put("clusterA1", clusterA1);
XdsTestUtils.addEdsClusters(clusterMap, edsMap, "clusterA11", "clusterA12");
Listener clientListener = buildInlineClientListener(RDS_NAME, "root");
controlPlaneService.setXdsConfig(ADS_TYPE_URL_LDS,
ImmutableMap.of(serverName, clientListener));
controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap);
controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap);
inOrder.verify(xdsConfigWatcher).onUpdate(any());
// Update the cluster to A -> A2 -> (A21, A22)
Cluster clusterA2 =
XdsTestUtils.buildAggCluster("clusterA2", Arrays.asList("clusterA21", "clusterA22"));
clusterA =
XdsTestUtils.buildAggCluster("clusterA", Arrays.asList("clusterA2"));
clusterMap.clear();
edsMap.clear();
clusterMap.put("root", rootCluster);
clusterMap.put("clusterA", clusterA);
clusterMap.put("clusterA2", clusterA2);
XdsTestUtils.addEdsClusters(clusterMap, edsMap, "clusterA21", "clusterA22");
controlPlaneService.setXdsConfig(ADS_TYPE_URL_CDS, clusterMap);
controlPlaneService.setXdsConfig(ADS_TYPE_URL_EDS, edsMap);
// Verify that the config is updated as expected
ClusterNameMatcher nameMatcher = new ClusterNameMatcher(Arrays.asList(
"root", "clusterA", "clusterA2", "clusterA21", "clusterA22"));
inOrder.verify(xdsConfigWatcher).onUpdate(argThat(nameMatcher));
}
@Test
public void testCdsError() throws IOException {
controlPlaneService.setXdsConfig(
ADS_TYPE_URL_CDS, ImmutableMap.of(XdsTestUtils.CLUSTER_NAME,
Cluster.newBuilder().setName(XdsTestUtils.CLUSTER_NAME).build()));
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
verify(xdsConfigWatcher).onUpdate(xdsUpdateCaptor.capture());
Status status = xdsUpdateCaptor.getValue().getValue()
.getClusters().get(CLUSTER_NAME).getStatus();
assertThat(status.getDescription()).contains(XdsTestUtils.CLUSTER_NAME);
}
@Test
public void ldsUpdateAfterShutdown() {
XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS",
ENDPOINT_HOSTNAME, ENDPOINT_PORT);
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
verify(xdsConfigWatcher).onUpdate(any());
@SuppressWarnings("unchecked")
XdsClient.ResourceWatcher<XdsListenerResource.LdsUpdate> resourceWatcher =
mock(XdsClient.ResourceWatcher.class);
xdsClient.watchXdsResource(
XdsListenerResource.getInstance(),
serverName,
resourceWatcher,
MoreExecutors.directExecutor());
verify(resourceWatcher).onChanged(any());
syncContext.execute(() -> {
// Shutdown before any updates. This will unsubscribe from XdsClient, but only after this
// Runnable returns
xdsDependencyManager.shutdown();
XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS2", "CDS", "EDS",
ENDPOINT_HOSTNAME, ENDPOINT_PORT);
verify(resourceWatcher, times(2)).onChanged(any());
xdsClient.cancelXdsResourceWatch(
XdsListenerResource.getInstance(), serverName, resourceWatcher);
});
}
@Test
public void rdsUpdateAfterShutdown() {
XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS",
ENDPOINT_HOSTNAME, ENDPOINT_PORT);
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
verify(xdsConfigWatcher).onUpdate(any());
@SuppressWarnings("unchecked")
XdsClient.ResourceWatcher<XdsRouteConfigureResource.RdsUpdate> resourceWatcher =
mock(XdsClient.ResourceWatcher.class);
xdsClient.watchXdsResource(
XdsRouteConfigureResource.getInstance(),
"RDS",
resourceWatcher,
MoreExecutors.directExecutor());
verify(resourceWatcher).onChanged(any());
syncContext.execute(() -> {
// Shutdown before any updates. This will unsubscribe from XdsClient, but only after this
// Runnable returns
xdsDependencyManager.shutdown();
XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS2", "EDS",
ENDPOINT_HOSTNAME, ENDPOINT_PORT);
verify(resourceWatcher, times(2)).onChanged(any());
xdsClient.cancelXdsResourceWatch(
XdsRouteConfigureResource.getInstance(), serverName, resourceWatcher);
});
}
@Test
public void cdsUpdateAfterShutdown() {
XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS",
ENDPOINT_HOSTNAME, ENDPOINT_PORT);
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
verify(xdsConfigWatcher).onUpdate(any());
@SuppressWarnings("unchecked")
XdsClient.ResourceWatcher<XdsClusterResource.CdsUpdate> resourceWatcher =
mock(XdsClient.ResourceWatcher.class);
xdsClient.watchXdsResource(
XdsClusterResource.getInstance(),
"CDS",
resourceWatcher,
MoreExecutors.directExecutor());
verify(resourceWatcher).onChanged(any());
syncContext.execute(() -> {
// Shutdown before any updates. This will unsubscribe from XdsClient, but only after this
// Runnable returns
xdsDependencyManager.shutdown();
XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS2",
ENDPOINT_HOSTNAME, ENDPOINT_PORT);
verify(resourceWatcher, times(2)).onChanged(any());
xdsClient.cancelXdsResourceWatch(
XdsClusterResource.getInstance(), serverName, resourceWatcher);
});
}
@Test
public void edsUpdateAfterShutdown() {
XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS",
ENDPOINT_HOSTNAME, ENDPOINT_PORT);
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
verify(xdsConfigWatcher).onUpdate(any());
@SuppressWarnings("unchecked")
XdsClient.ResourceWatcher<XdsEndpointResource.EdsUpdate> resourceWatcher =
mock(XdsClient.ResourceWatcher.class);
xdsClient.watchXdsResource(
XdsEndpointResource.getInstance(),
"EDS",
resourceWatcher,
MoreExecutors.directExecutor());
verify(resourceWatcher).onChanged(any());
syncContext.execute(() -> {
// Shutdown before any updates. This will unsubscribe from XdsClient, but only after this
// Runnable returns
xdsDependencyManager.shutdown();
XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS",
ENDPOINT_HOSTNAME + "2", ENDPOINT_PORT);
verify(resourceWatcher, times(2)).onChanged(any());
xdsClient.cancelXdsResourceWatch(
XdsEndpointResource.getInstance(), serverName, resourceWatcher);
});
}
@Test
public void subscribeToClusterAfterShutdown() throws Exception {
XdsTestUtils.setAdsConfig(controlPlaneService, serverName, "RDS", "CDS", "EDS",
ENDPOINT_HOSTNAME, ENDPOINT_PORT);
InOrder inOrder = Mockito.inOrder(xdsConfigWatcher);
xdsDependencyManager = new XdsDependencyManager(xdsClient, xdsConfigWatcher, syncContext,
serverName, serverName, nameResolverArgs, scheduler);
inOrder.verify(xdsConfigWatcher).onUpdate(any());
xdsDependencyManager.shutdown();
Closeable subscription = xdsDependencyManager.subscribeToCluster("CDS");
inOrder.verify(xdsConfigWatcher, never()).onUpdate(any());
subscription.close();
}
private Listener buildInlineClientListener(String rdsName, String clusterName) {
return XdsTestUtils.buildInlineClientListener(rdsName, clusterName, serverName);
}
private static class TestWatcher implements XdsDependencyManager.XdsConfigWatcher {
XdsConfig lastConfig;
int numUpdates = 0;
int numError = 0;
@Override
public void onUpdate(StatusOr<XdsConfig> update) {
log.fine("Config update: " + update);
if (update.hasValue()) {
lastConfig = update.getValue();
numUpdates++;
} else {
numError++;
}
}
private List<Integer> getStats() {
return Arrays.asList(numUpdates, numError);
}
private void verifyStats(int updt, int err) {
assertThat(getStats()).isEqualTo(Arrays.asList(updt, err));
}
}
static class ClusterNameMatcher implements ArgumentMatcher<StatusOr<XdsConfig>> {
private final List<String> expectedNames;
ClusterNameMatcher(List<String> expectedNames) {
this.expectedNames = expectedNames;
}
@Override
public boolean matches(StatusOr<XdsConfig> update) {
if (!update.hasValue()) {
return false;
}
XdsConfig xdsConfig = update.getValue();
if (xdsConfig == null || xdsConfig.getClusters() == null) {
return false;
}
return xdsConfig.getClusters().size() == expectedNames.size()
&& xdsConfig.getClusters().keySet().containsAll(expectedNames);
}
}
}