diff --git a/xds/src/generated/main/grpc/io/envoyproxy/envoy/service/discovery/v3/AggregatedDiscoveryServiceGrpc.java b/xds/src/generated/main/grpc/io/envoyproxy/envoy/service/discovery/v3/AggregatedDiscoveryServiceGrpc.java new file mode 100644 index 00000000000..a2698b43853 --- /dev/null +++ b/xds/src/generated/main/grpc/io/envoyproxy/envoy/service/discovery/v3/AggregatedDiscoveryServiceGrpc.java @@ -0,0 +1,376 @@ +package io.envoyproxy.envoy.service.discovery.v3; + +import static io.grpc.MethodDescriptor.generateFullMethodName; +import static io.grpc.stub.ClientCalls.asyncBidiStreamingCall; +import static io.grpc.stub.ClientCalls.asyncClientStreamingCall; +import static io.grpc.stub.ClientCalls.asyncServerStreamingCall; +import static io.grpc.stub.ClientCalls.asyncUnaryCall; +import static io.grpc.stub.ClientCalls.blockingServerStreamingCall; +import static io.grpc.stub.ClientCalls.blockingUnaryCall; +import static io.grpc.stub.ClientCalls.futureUnaryCall; +import static io.grpc.stub.ServerCalls.asyncBidiStreamingCall; +import static io.grpc.stub.ServerCalls.asyncClientStreamingCall; +import static io.grpc.stub.ServerCalls.asyncServerStreamingCall; +import static io.grpc.stub.ServerCalls.asyncUnaryCall; +import static io.grpc.stub.ServerCalls.asyncUnimplementedStreamingCall; +import static io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall; + +/** + *
+ * See https://github.com/lyft/envoy-api#apis for a description of the role of
+ * ADS and how it is intended to be used by a management server. ADS requests
+ * have the same structure as their singleton xDS counterparts, but can
+ * multiplex many resource types on a single stream. The type_url in the
+ * DiscoveryRequest/DiscoveryResponse provides sufficient information to recover
+ * the multiplexed singleton APIs at the Envoy instance and management server.
+ * 
+ */ +@javax.annotation.Generated( + value = "by gRPC proto compiler", + comments = "Source: envoy/service/discovery/v3/ads.proto") +public final class AggregatedDiscoveryServiceGrpc { + + private AggregatedDiscoveryServiceGrpc() {} + + public static final String SERVICE_NAME = "envoy.service.discovery.v3.AggregatedDiscoveryService"; + + // Static method descriptors that strictly reflect the proto. + private static volatile io.grpc.MethodDescriptor getStreamAggregatedResourcesMethod; + + @io.grpc.stub.annotations.RpcMethod( + fullMethodName = SERVICE_NAME + '/' + "StreamAggregatedResources", + requestType = io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest.class, + responseType = io.envoyproxy.envoy.service.discovery.v3.DiscoveryResponse.class, + methodType = io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING) + public static io.grpc.MethodDescriptor getStreamAggregatedResourcesMethod() { + io.grpc.MethodDescriptor getStreamAggregatedResourcesMethod; + if ((getStreamAggregatedResourcesMethod = AggregatedDiscoveryServiceGrpc.getStreamAggregatedResourcesMethod) == null) { + synchronized (AggregatedDiscoveryServiceGrpc.class) { + if ((getStreamAggregatedResourcesMethod = AggregatedDiscoveryServiceGrpc.getStreamAggregatedResourcesMethod) == null) { + AggregatedDiscoveryServiceGrpc.getStreamAggregatedResourcesMethod = getStreamAggregatedResourcesMethod = + io.grpc.MethodDescriptor.newBuilder() + .setType(io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING) + .setFullMethodName(generateFullMethodName(SERVICE_NAME, "StreamAggregatedResources")) + .setSampledToLocalTracing(true) + .setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest.getDefaultInstance())) + .setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + io.envoyproxy.envoy.service.discovery.v3.DiscoveryResponse.getDefaultInstance())) + .setSchemaDescriptor(new AggregatedDiscoveryServiceMethodDescriptorSupplier("StreamAggregatedResources")) + .build(); + } + } + } + return getStreamAggregatedResourcesMethod; + } + + private static volatile io.grpc.MethodDescriptor getDeltaAggregatedResourcesMethod; + + @io.grpc.stub.annotations.RpcMethod( + fullMethodName = SERVICE_NAME + '/' + "DeltaAggregatedResources", + requestType = io.envoyproxy.envoy.service.discovery.v3.DeltaDiscoveryRequest.class, + responseType = io.envoyproxy.envoy.service.discovery.v3.DeltaDiscoveryResponse.class, + methodType = io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING) + public static io.grpc.MethodDescriptor getDeltaAggregatedResourcesMethod() { + io.grpc.MethodDescriptor getDeltaAggregatedResourcesMethod; + if ((getDeltaAggregatedResourcesMethod = AggregatedDiscoveryServiceGrpc.getDeltaAggregatedResourcesMethod) == null) { + synchronized (AggregatedDiscoveryServiceGrpc.class) { + if ((getDeltaAggregatedResourcesMethod = AggregatedDiscoveryServiceGrpc.getDeltaAggregatedResourcesMethod) == null) { + AggregatedDiscoveryServiceGrpc.getDeltaAggregatedResourcesMethod = getDeltaAggregatedResourcesMethod = + io.grpc.MethodDescriptor.newBuilder() + .setType(io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING) + .setFullMethodName(generateFullMethodName(SERVICE_NAME, "DeltaAggregatedResources")) + .setSampledToLocalTracing(true) + .setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + io.envoyproxy.envoy.service.discovery.v3.DeltaDiscoveryRequest.getDefaultInstance())) + .setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller( + io.envoyproxy.envoy.service.discovery.v3.DeltaDiscoveryResponse.getDefaultInstance())) + .setSchemaDescriptor(new AggregatedDiscoveryServiceMethodDescriptorSupplier("DeltaAggregatedResources")) + .build(); + } + } + } + return getDeltaAggregatedResourcesMethod; + } + + /** + * Creates a new async stub that supports all call types for the service + */ + public static AggregatedDiscoveryServiceStub newStub(io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public AggregatedDiscoveryServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AggregatedDiscoveryServiceStub(channel, callOptions); + } + }; + return AggregatedDiscoveryServiceStub.newStub(factory, channel); + } + + /** + * Creates a new blocking-style stub that supports unary and streaming output calls on the service + */ + public static AggregatedDiscoveryServiceBlockingStub newBlockingStub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public AggregatedDiscoveryServiceBlockingStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AggregatedDiscoveryServiceBlockingStub(channel, callOptions); + } + }; + return AggregatedDiscoveryServiceBlockingStub.newStub(factory, channel); + } + + /** + * Creates a new ListenableFuture-style stub that supports unary calls on the service + */ + public static AggregatedDiscoveryServiceFutureStub newFutureStub( + io.grpc.Channel channel) { + io.grpc.stub.AbstractStub.StubFactory factory = + new io.grpc.stub.AbstractStub.StubFactory() { + @java.lang.Override + public AggregatedDiscoveryServiceFutureStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AggregatedDiscoveryServiceFutureStub(channel, callOptions); + } + }; + return AggregatedDiscoveryServiceFutureStub.newStub(factory, channel); + } + + /** + *
+   * See https://github.com/lyft/envoy-api#apis for a description of the role of
+   * ADS and how it is intended to be used by a management server. ADS requests
+   * have the same structure as their singleton xDS counterparts, but can
+   * multiplex many resource types on a single stream. The type_url in the
+   * DiscoveryRequest/DiscoveryResponse provides sufficient information to recover
+   * the multiplexed singleton APIs at the Envoy instance and management server.
+   * 
+ */ + public static abstract class AggregatedDiscoveryServiceImplBase implements io.grpc.BindableService { + + /** + *
+     * This is a gRPC-only API.
+     * 
+ */ + public io.grpc.stub.StreamObserver streamAggregatedResources( + io.grpc.stub.StreamObserver responseObserver) { + return asyncUnimplementedStreamingCall(getStreamAggregatedResourcesMethod(), responseObserver); + } + + /** + */ + public io.grpc.stub.StreamObserver deltaAggregatedResources( + io.grpc.stub.StreamObserver responseObserver) { + return asyncUnimplementedStreamingCall(getDeltaAggregatedResourcesMethod(), responseObserver); + } + + @java.lang.Override public final io.grpc.ServerServiceDefinition bindService() { + return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor()) + .addMethod( + getStreamAggregatedResourcesMethod(), + asyncBidiStreamingCall( + new MethodHandlers< + io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest, + io.envoyproxy.envoy.service.discovery.v3.DiscoveryResponse>( + this, METHODID_STREAM_AGGREGATED_RESOURCES))) + .addMethod( + getDeltaAggregatedResourcesMethod(), + asyncBidiStreamingCall( + new MethodHandlers< + io.envoyproxy.envoy.service.discovery.v3.DeltaDiscoveryRequest, + io.envoyproxy.envoy.service.discovery.v3.DeltaDiscoveryResponse>( + this, METHODID_DELTA_AGGREGATED_RESOURCES))) + .build(); + } + } + + /** + *
+   * See https://github.com/lyft/envoy-api#apis for a description of the role of
+   * ADS and how it is intended to be used by a management server. ADS requests
+   * have the same structure as their singleton xDS counterparts, but can
+   * multiplex many resource types on a single stream. The type_url in the
+   * DiscoveryRequest/DiscoveryResponse provides sufficient information to recover
+   * the multiplexed singleton APIs at the Envoy instance and management server.
+   * 
+ */ + public static final class AggregatedDiscoveryServiceStub extends io.grpc.stub.AbstractAsyncStub { + private AggregatedDiscoveryServiceStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected AggregatedDiscoveryServiceStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AggregatedDiscoveryServiceStub(channel, callOptions); + } + + /** + *
+     * This is a gRPC-only API.
+     * 
+ */ + public io.grpc.stub.StreamObserver streamAggregatedResources( + io.grpc.stub.StreamObserver responseObserver) { + return asyncBidiStreamingCall( + getChannel().newCall(getStreamAggregatedResourcesMethod(), getCallOptions()), responseObserver); + } + + /** + */ + public io.grpc.stub.StreamObserver deltaAggregatedResources( + io.grpc.stub.StreamObserver responseObserver) { + return asyncBidiStreamingCall( + getChannel().newCall(getDeltaAggregatedResourcesMethod(), getCallOptions()), responseObserver); + } + } + + /** + *
+   * See https://github.com/lyft/envoy-api#apis for a description of the role of
+   * ADS and how it is intended to be used by a management server. ADS requests
+   * have the same structure as their singleton xDS counterparts, but can
+   * multiplex many resource types on a single stream. The type_url in the
+   * DiscoveryRequest/DiscoveryResponse provides sufficient information to recover
+   * the multiplexed singleton APIs at the Envoy instance and management server.
+   * 
+ */ + public static final class AggregatedDiscoveryServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { + private AggregatedDiscoveryServiceBlockingStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected AggregatedDiscoveryServiceBlockingStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AggregatedDiscoveryServiceBlockingStub(channel, callOptions); + } + } + + /** + *
+   * See https://github.com/lyft/envoy-api#apis for a description of the role of
+   * ADS and how it is intended to be used by a management server. ADS requests
+   * have the same structure as their singleton xDS counterparts, but can
+   * multiplex many resource types on a single stream. The type_url in the
+   * DiscoveryRequest/DiscoveryResponse provides sufficient information to recover
+   * the multiplexed singleton APIs at the Envoy instance and management server.
+   * 
+ */ + public static final class AggregatedDiscoveryServiceFutureStub extends io.grpc.stub.AbstractFutureStub { + private AggregatedDiscoveryServiceFutureStub( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + super(channel, callOptions); + } + + @java.lang.Override + protected AggregatedDiscoveryServiceFutureStub build( + io.grpc.Channel channel, io.grpc.CallOptions callOptions) { + return new AggregatedDiscoveryServiceFutureStub(channel, callOptions); + } + } + + private static final int METHODID_STREAM_AGGREGATED_RESOURCES = 0; + private static final int METHODID_DELTA_AGGREGATED_RESOURCES = 1; + + private static final class MethodHandlers implements + io.grpc.stub.ServerCalls.UnaryMethod, + io.grpc.stub.ServerCalls.ServerStreamingMethod, + io.grpc.stub.ServerCalls.ClientStreamingMethod, + io.grpc.stub.ServerCalls.BidiStreamingMethod { + private final AggregatedDiscoveryServiceImplBase serviceImpl; + private final int methodId; + + MethodHandlers(AggregatedDiscoveryServiceImplBase serviceImpl, int methodId) { + this.serviceImpl = serviceImpl; + this.methodId = methodId; + } + + @java.lang.Override + @java.lang.SuppressWarnings("unchecked") + public void invoke(Req request, io.grpc.stub.StreamObserver responseObserver) { + switch (methodId) { + default: + throw new AssertionError(); + } + } + + @java.lang.Override + @java.lang.SuppressWarnings("unchecked") + public io.grpc.stub.StreamObserver invoke( + io.grpc.stub.StreamObserver responseObserver) { + switch (methodId) { + case METHODID_STREAM_AGGREGATED_RESOURCES: + return (io.grpc.stub.StreamObserver) serviceImpl.streamAggregatedResources( + (io.grpc.stub.StreamObserver) responseObserver); + case METHODID_DELTA_AGGREGATED_RESOURCES: + return (io.grpc.stub.StreamObserver) serviceImpl.deltaAggregatedResources( + (io.grpc.stub.StreamObserver) responseObserver); + default: + throw new AssertionError(); + } + } + } + + private static abstract class AggregatedDiscoveryServiceBaseDescriptorSupplier + implements io.grpc.protobuf.ProtoFileDescriptorSupplier, io.grpc.protobuf.ProtoServiceDescriptorSupplier { + AggregatedDiscoveryServiceBaseDescriptorSupplier() {} + + @java.lang.Override + public com.google.protobuf.Descriptors.FileDescriptor getFileDescriptor() { + return io.envoyproxy.envoy.service.discovery.v3.AdsProto.getDescriptor(); + } + + @java.lang.Override + public com.google.protobuf.Descriptors.ServiceDescriptor getServiceDescriptor() { + return getFileDescriptor().findServiceByName("AggregatedDiscoveryService"); + } + } + + private static final class AggregatedDiscoveryServiceFileDescriptorSupplier + extends AggregatedDiscoveryServiceBaseDescriptorSupplier { + AggregatedDiscoveryServiceFileDescriptorSupplier() {} + } + + private static final class AggregatedDiscoveryServiceMethodDescriptorSupplier + extends AggregatedDiscoveryServiceBaseDescriptorSupplier + implements io.grpc.protobuf.ProtoMethodDescriptorSupplier { + private final String methodName; + + AggregatedDiscoveryServiceMethodDescriptorSupplier(String methodName) { + this.methodName = methodName; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.MethodDescriptor getMethodDescriptor() { + return getServiceDescriptor().findMethodByName(methodName); + } + } + + private static volatile io.grpc.ServiceDescriptor serviceDescriptor; + + public static io.grpc.ServiceDescriptor getServiceDescriptor() { + io.grpc.ServiceDescriptor result = serviceDescriptor; + if (result == null) { + synchronized (AggregatedDiscoveryServiceGrpc.class) { + result = serviceDescriptor; + if (result == null) { + serviceDescriptor = result = io.grpc.ServiceDescriptor.newBuilder(SERVICE_NAME) + .setSchemaDescriptor(new AggregatedDiscoveryServiceFileDescriptorSupplier()) + .addMethod(getStreamAggregatedResourcesMethod()) + .addMethod(getDeltaAggregatedResourcesMethod()) + .build(); + } + } + } + return result; + } +} diff --git a/xds/src/main/java/io/grpc/xds/Bootstrapper.java b/xds/src/main/java/io/grpc/xds/Bootstrapper.java index cceb900c027..6fa3c4e5710 100644 --- a/xds/src/main/java/io/grpc/xds/Bootstrapper.java +++ b/xds/src/main/java/io/grpc/xds/Bootstrapper.java @@ -17,17 +17,13 @@ package io.grpc.xds; import com.google.common.annotations.VisibleForTesting; -import com.google.protobuf.ListValue; -import com.google.protobuf.NullValue; -import com.google.protobuf.Struct; -import com.google.protobuf.Value; -import io.envoyproxy.envoy.api.v2.core.Locality; -import io.envoyproxy.envoy.api.v2.core.Node; import io.grpc.Internal; import io.grpc.internal.GrpcUtil; import io.grpc.internal.GrpcUtil.GrpcBuildVersion; import io.grpc.internal.JsonParser; import io.grpc.internal.JsonUtil; +import io.grpc.xds.EnvoyProtoData.Locality; +import io.grpc.xds.EnvoyProtoData.Node; import io.grpc.xds.XdsLogger.XdsLogLevel; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -115,7 +111,11 @@ static BootstrapInfo parseConfig(String rawData) throws IOException { channelCredsOptions.add(creds); } } - servers.add(new ServerInfo(serverUri, channelCredsOptions)); + List serverFeatures = JsonUtil.getListOfStrings(serverConfig, "server_features"); + if (serverFeatures != null) { + logger.log(XdsLogLevel.INFO, "Server features: {0}", serverFeatures); + } + servers.add(new ServerInfo(serverUri, channelCredsOptions, serverFeatures)); } Node.Builder nodeBuilder = Node.newBuilder(); @@ -133,34 +133,24 @@ static BootstrapInfo parseConfig(String rawData) throws IOException { } Map metadata = JsonUtil.getObject(rawNode, "metadata"); if (metadata != null) { - Struct.Builder structBuilder = Struct.newBuilder(); - for (Map.Entry entry : metadata.entrySet()) { - logger.log( - XdsLogLevel.INFO, - "Node metadata field {0}: {1}", entry.getKey(), entry.getValue()); - structBuilder.putFields(entry.getKey(), convertToValue(entry.getValue())); - } - nodeBuilder.setMetadata(structBuilder); + nodeBuilder.setMetadata(metadata); } Map rawLocality = JsonUtil.getObject(rawNode, "locality"); if (rawLocality != null) { - Locality.Builder localityBuilder = Locality.newBuilder(); - if (rawLocality.containsKey("region")) { - String region = JsonUtil.getString(rawLocality, "region"); + String region = JsonUtil.getString(rawLocality, "region"); + String zone = JsonUtil.getString(rawLocality, "zone"); + String subZone = JsonUtil.getString(rawLocality, "sub_zone"); + if (region != null) { logger.log(XdsLogLevel.INFO, "Locality region: {0}", region); - localityBuilder.setRegion(region); } if (rawLocality.containsKey("zone")) { - String zone = JsonUtil.getString(rawLocality, "zone"); logger.log(XdsLogLevel.INFO, "Locality zone: {0}", zone); - localityBuilder.setZone(zone); } if (rawLocality.containsKey("sub_zone")) { - String subZone = JsonUtil.getString(rawLocality, "sub_zone"); logger.log(XdsLogLevel.INFO, "Locality sub_zone: {0}", subZone); - localityBuilder.setSubZone(subZone); } - nodeBuilder.setLocality(localityBuilder); + Locality locality = new Locality(region, zone, subZone); + nodeBuilder.setLocality(locality); } } GrpcBuildVersion buildVersion = GrpcUtil.getGrpcBuildVersion(); @@ -173,43 +163,6 @@ static BootstrapInfo parseConfig(String rawData) throws IOException { return new BootstrapInfo(servers, nodeBuilder.build()); } - /** - * Converts Java representation of the given JSON value to protobuf's {@link - * com.google.protobuf.Value} representation. - * - *

The given {@code rawObject} must be a valid JSON value in Java representation, which is - * either a {@code Map}, {@code List}, {@code String}, {@code Double}, - * {@code Boolean}, or {@code null}. - */ - private static Value convertToValue(Object rawObject) { - Value.Builder valueBuilder = Value.newBuilder(); - if (rawObject == null) { - valueBuilder.setNullValue(NullValue.NULL_VALUE); - } else if (rawObject instanceof Double) { - valueBuilder.setNumberValue((Double) rawObject); - } else if (rawObject instanceof String) { - valueBuilder.setStringValue((String) rawObject); - } else if (rawObject instanceof Boolean) { - valueBuilder.setBoolValue((Boolean) rawObject); - } else if (rawObject instanceof Map) { - Struct.Builder structBuilder = Struct.newBuilder(); - @SuppressWarnings("unchecked") - Map map = (Map) rawObject; - for (Map.Entry entry : map.entrySet()) { - structBuilder.putFields(entry.getKey(), convertToValue(entry.getValue())); - } - valueBuilder.setStructValue(structBuilder); - } else if (rawObject instanceof List) { - ListValue.Builder listBuilder = ListValue.newBuilder(); - List list = (List) rawObject; - for (Object obj : list) { - listBuilder.addValues(convertToValue(obj)); - } - valueBuilder.setListValue(listBuilder); - } - return valueBuilder.build(); - } - /** * Data class containing channel credentials configurations for xDS protocol communication. */ @@ -247,11 +200,14 @@ String getType() { static class ServerInfo { private final String serverUri; private final List channelCredsList; + @Nullable + private final List serverFeatures; @VisibleForTesting - ServerInfo(String serverUri, List channelCredsList) { + ServerInfo(String serverUri, List channelCredsList, List serverFeatures) { this.serverUri = serverUri; this.channelCredsList = channelCredsList; + this.serverFeatures = serverFeatures; } String getServerUri() { @@ -261,6 +217,12 @@ String getServerUri() { List getChannelCredentials() { return Collections.unmodifiableList(channelCredsList); } + + List getServerFeatures() { + return serverFeatures == null + ? Collections.emptyList() + : Collections.unmodifiableList(serverFeatures); + } } /** @@ -291,6 +253,5 @@ List getServers() { public Node getNode() { return node; } - } } diff --git a/xds/src/main/java/io/grpc/xds/ClientLoadCounter.java b/xds/src/main/java/io/grpc/xds/ClientLoadCounter.java index 8e8754d2542..f86002b683f 100644 --- a/xds/src/main/java/io/grpc/xds/ClientLoadCounter.java +++ b/xds/src/main/java/io/grpc/xds/ClientLoadCounter.java @@ -54,27 +54,10 @@ final class ClientLoadCounter { private final AtomicLong callsIssued = new AtomicLong(); private final MetricRecorder[] metricRecorders = new MetricRecorder[THREAD_BALANCING_FACTOR]; - // True if this counter continues to record stats after next snapshot. Otherwise, it will be - // discarded. - private boolean active; - ClientLoadCounter() { for (int i = 0; i < THREAD_BALANCING_FACTOR; i++) { metricRecorders[i] = new MetricRecorder(); } - active = true; - } - - /** - * Must only be used for testing. - */ - @VisibleForTesting - ClientLoadCounter(long callsSucceeded, long callsInProgress, long callsFailed, long callsIssued) { - this(); - this.callsSucceeded.set(callsSucceeded); - this.callsInProgress.set(callsInProgress); - this.callsFailed.set(callsFailed); - this.callsIssued.set(callsIssued); } void recordCallStarted() { @@ -98,12 +81,8 @@ void recordMetric(String name, double value) { } /** - * Generates a snapshot for load stats recorded in this counter. Successive snapshots represent - * load stats recorded for the interval since the previous snapshot. So taking a snapshot clears - * the counter state except for ongoing RPC recordings. - * - *

This method is not thread-safe and must be called from {@link - * io.grpc.LoadBalancer.Helper#getSynchronizationContext()}. + * Generates a snapshot for load stats recorded in this counter for the interval between calls + * of this method. */ ClientLoadSnapshot snapshot() { Map aggregatedValues = new HashMap<>(); @@ -127,12 +106,24 @@ ClientLoadSnapshot snapshot() { aggregatedValues); } - void setActive(boolean value) { - active = value; + @VisibleForTesting + void setCallsIssued(long callsIssued) { + this.callsIssued.set(callsIssued); + } + + @VisibleForTesting + void setCallsInProgress(long callsInProgress) { + this.callsInProgress.set(callsInProgress); } - boolean isActive() { - return active; + @VisibleForTesting + void setCallsSucceeded(long callsSucceeded) { + this.callsSucceeded.set(callsSucceeded); + } + + @VisibleForTesting + void setCallsFailed(long callsFailed) { + this.callsFailed.set(callsFailed); } /** diff --git a/xds/src/main/java/io/grpc/xds/EdsLoadBalancer.java b/xds/src/main/java/io/grpc/xds/EdsLoadBalancer.java index 34e90c7c7a1..b009ea7d530 100644 --- a/xds/src/main/java/io/grpc/xds/EdsLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/EdsLoadBalancer.java @@ -22,7 +22,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import io.envoyproxy.envoy.api.v2.core.Node; import io.grpc.Attributes; import io.grpc.InternalLogId; import io.grpc.LoadBalancer; @@ -38,6 +37,8 @@ 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.LoadStatsManager.LoadStatsStore; import io.grpc.xds.LocalityStore.LocalityStoreFactory; import io.grpc.xds.XdsClient.EndpointUpdate; import io.grpc.xds.XdsClient.EndpointWatcher; @@ -208,11 +209,9 @@ public void shutdown() { */ private final class ClusterEndpointsBalancerFactory extends LoadBalancer.Factory { @Nullable final String clusterServiceName; - final LoadStatsStore loadStatsStore; ClusterEndpointsBalancerFactory(@Nullable String clusterServiceName) { this.clusterServiceName = clusterServiceName; - loadStatsStore = new LoadStatsStoreImpl(clusterName, clusterServiceName); } @Override @@ -248,6 +247,7 @@ final class ClusterEndpointsBalancer extends LoadBalancer { ClusterEndpointsBalancer(Helper helper) { this.helper = helper; resourceName = clusterServiceName != null ? clusterServiceName : clusterName; + LoadStatsStore loadStatsStore = xdsClient.addClientStats(clusterName, clusterServiceName); localityStore = localityStoreFactory.newLocalityStore(logId, helper, lbRegistry, loadStatsStore); endpointWatcher = new EndpointWatcherImpl(); @@ -267,22 +267,12 @@ public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) { throw new AssertionError("Can only report load to the same management server"); } if (!isReportingLoad) { - logger.log( - XdsLogLevel.INFO, - "Start reporting loads for cluster: {0}, cluster_service: {1}", - clusterName, - clusterServiceName); - xdsClient.reportClientStats(clusterName, clusterServiceName, loadStatsStore); + xdsClient.reportClientStats(); isReportingLoad = true; } } else { if (isReportingLoad) { - logger.log( - XdsLogLevel.INFO, - "Stop reporting loads for cluster: {0}, cluster_service: {1}", - clusterName, - clusterServiceName); - xdsClient.cancelClientStatsReport(clusterName, clusterServiceName); + xdsClient.cancelClientStatsReport(); isReportingLoad = false; } } @@ -304,15 +294,11 @@ public boolean canHandleEmptyAddressListFromNameResolution() { @Override public void shutdown() { if (isReportingLoad) { - logger.log( - XdsLogLevel.INFO, - "Stop reporting loads for cluster: {0}, cluster_service: {1}", - clusterName, - clusterServiceName); - xdsClient.cancelClientStatsReport(clusterName, clusterServiceName); + xdsClient.cancelClientStatsReport(); isReportingLoad = false; } localityStore.reset(); + xdsClient.removeClientStats(clusterName, clusterServiceName); xdsClient.cancelEndpointDataWatch(resourceName, endpointWatcher); logger.log( XdsLogLevel.INFO, @@ -365,12 +351,7 @@ public void onEndpointChanged(EndpointUpdate endpointUpdate) { public void onResourceDoesNotExist(String resourceName) { logger.log(XdsLogLevel.INFO, "Resource {0} is unavailable", resourceName); if (isReportingLoad) { - logger.log( - XdsLogLevel.INFO, - "Stop reporting loads for cluster: {0}, cluster_service: {1}", - clusterName, - clusterServiceName); - xdsClient.cancelClientStatsReport(clusterName, clusterServiceName); + xdsClient.cancelClientStatsReport(); isReportingLoad = false; } localityStore.reset(); diff --git a/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java b/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java index be691337f22..14a62f29f5a 100644 --- a/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java +++ b/xds/src/main/java/io/grpc/xds/EnvoyProtoData.java @@ -22,6 +22,11 @@ import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects.ToStringHelper; import com.google.common.collect.ImmutableList; +import com.google.protobuf.ListValue; +import com.google.protobuf.NullValue; +import com.google.protobuf.Struct; +import com.google.protobuf.Value; +import com.google.protobuf.util.Durations; import com.google.re2j.Pattern; import com.google.re2j.PatternSyntaxException; import io.envoyproxy.envoy.type.v3.FractionalPercent; @@ -34,7 +39,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; /** @@ -135,17 +142,345 @@ public String toString() { } /** - * See corresponding Envoy proto message {@link io.envoyproxy.envoy.api.v2.core.Locality}. + * See corresponding Envoy proto message {@link io.envoyproxy.envoy.config.core.v3.Node}. + */ + public static final class Node { + + private final String id; + private final String cluster; + @Nullable + private final Map metadata; + @Nullable + private final Locality locality; + private final List

listeningAddresses; + private final String buildVersion; + private final String userAgentName; + @Nullable + private final String userAgentVersion; + private final List clientFeatures; + + private Node( + String id, String cluster, @Nullable Map metadata, @Nullable Locality locality, + List
listeningAddresses, String buildVersion, String userAgentName, + @Nullable String userAgentVersion, List clientFeatures) { + this.id = checkNotNull(id, "id"); + this.cluster = checkNotNull(cluster, "cluster"); + this.metadata = metadata; + this.locality = locality; + this.listeningAddresses = Collections.unmodifiableList( + checkNotNull(listeningAddresses, "listeningAddresses")); + this.buildVersion = checkNotNull(buildVersion, "buildVersion"); + this.userAgentName = checkNotNull(userAgentName, "userAgentName"); + this.userAgentVersion = userAgentVersion; + this.clientFeatures = Collections.unmodifiableList( + checkNotNull(clientFeatures, "clientFeatures")); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("cluster", cluster) + .add("metadata", metadata) + .add("locality", locality) + .add("listeningAddresses", listeningAddresses) + .add("buildVersion", buildVersion) + .add("userAgentName", userAgentName) + .add("userAgentVersion", userAgentVersion) + .add("clientFeatures", clientFeatures) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Node node = (Node) o; + return Objects.equals(id, node.id) + && Objects.equals(cluster, node.cluster) + && Objects.equals(metadata, node.metadata) + && Objects.equals(locality, node.locality) + && Objects.equals(listeningAddresses, node.listeningAddresses) + && Objects.equals(buildVersion, node.buildVersion) + && Objects.equals(userAgentName, node.userAgentName) + && Objects.equals(userAgentVersion, node.userAgentVersion) + && Objects.equals(clientFeatures, node.clientFeatures); + } + + @Override + public int hashCode() { + return Objects + .hash(id, cluster, metadata, locality, listeningAddresses, buildVersion, userAgentName, + userAgentVersion, clientFeatures); + } + + static final class Builder { + private String id = ""; + private String cluster = ""; + @Nullable + private Map metadata; + @Nullable + private Locality locality; + private final List
listeningAddresses = new ArrayList<>(); + private String buildVersion = ""; + private String userAgentName = ""; + @Nullable + private String userAgentVersion; + private final List clientFeatures = new ArrayList<>(); + + private Builder() { + } + + Builder setId(String id) { + this.id = checkNotNull(id, "id"); + return this; + } + + Builder setCluster(String cluster) { + this.cluster = checkNotNull(cluster, "cluster"); + return this; + } + + Builder setMetadata(Map metadata) { + this.metadata = checkNotNull(metadata, "metadata"); + return this; + } + + Builder setLocality(Locality locality) { + this.locality = checkNotNull(locality, "locality"); + return this; + } + + Builder addListeningAddresses(Address address) { + listeningAddresses.add(checkNotNull(address, "address")); + return this; + } + + Builder setBuildVersion(String buildVersion) { + this.buildVersion = checkNotNull(buildVersion, "buildVersion"); + return this; + } + + Builder setUserAgentName(String userAgentName) { + this.userAgentName = checkNotNull(userAgentName, "userAgentName"); + return this; + } + + Builder setUserAgentVersion(String userAgentVersion) { + this.userAgentVersion = checkNotNull(userAgentVersion, "userAgentVersion"); + return this; + } + + Builder addClientFeatures(String clientFeature) { + this.clientFeatures.add(checkNotNull(clientFeature, "clientFeature")); + return this; + } + + Node build() { + return new Node( + id, cluster, metadata, locality, listeningAddresses, buildVersion, userAgentName, + userAgentVersion, clientFeatures); + } + } + + static Builder newBuilder() { + return new Builder(); + } + + Builder toBuilder() { + Builder builder = new Builder().setId(id).setCluster(cluster); + if (metadata != null) { + builder.setMetadata(metadata); + } + if (locality != null) { + builder.setLocality(locality); + } + builder.listeningAddresses.addAll(listeningAddresses); + return builder; + } + + String getId() { + return id; + } + + String getCluster() { + return cluster; + } + + @Nullable + Map getMetadata() { + return metadata; + } + + @Nullable + Locality getLocality() { + return locality; + } + + List
getListeningAddresses() { + return listeningAddresses; + } + + io.envoyproxy.envoy.config.core.v3.Node toEnvoyProtoNode() { + io.envoyproxy.envoy.config.core.v3.Node.Builder builder = + io.envoyproxy.envoy.config.core.v3.Node.newBuilder(); + builder.setId(id); + builder.setCluster(cluster); + if (metadata != null) { + Struct.Builder structBuilder = Struct.newBuilder(); + for (Map.Entry entry : metadata.entrySet()) { + structBuilder.putFields(entry.getKey(), convertToValue(entry.getValue())); + } + builder.setMetadata(structBuilder); + } + if (locality != null) { + builder.setLocality(locality.toEnvoyProtoLocality()); + } + for (Address address : listeningAddresses) { + builder.addListeningAddresses(address.toEnvoyProtoAddress()); + } + builder.setUserAgentName(userAgentName); + if (userAgentVersion != null) { + builder.setUserAgentVersion(userAgentVersion); + } + builder.addAllClientFeatures(clientFeatures); + return builder.build(); + } + + @SuppressWarnings("deprecation") // Deprecated v2 API setBuildVersion(). + public io.envoyproxy.envoy.api.v2.core.Node toEnvoyProtoNodeV2() { + io.envoyproxy.envoy.api.v2.core.Node.Builder builder = + io.envoyproxy.envoy.api.v2.core.Node.newBuilder(); + builder.setId(id); + builder.setCluster(cluster); + if (metadata != null) { + Struct.Builder structBuilder = Struct.newBuilder(); + for (Map.Entry entry : metadata.entrySet()) { + structBuilder.putFields(entry.getKey(), convertToValue(entry.getValue())); + } + builder.setMetadata(structBuilder); + } + if (locality != null) { + builder.setLocality(locality.toEnvoyProtoLocalityV2()); + } + for (Address address : listeningAddresses) { + builder.addListeningAddresses(address.toEnvoyProtoAddressV2()); + } + builder.setBuildVersion(buildVersion); + builder.setUserAgentName(userAgentName); + if (userAgentVersion != null) { + builder.setUserAgentVersion(userAgentVersion); + } + builder.addAllClientFeatures(clientFeatures); + return builder.build(); + } + } + + /** + * Converts Java representation of the given JSON value to protobuf's {@link + * com.google.protobuf.Value} representation. + * + *

The given {@code rawObject} must be a valid JSON value in Java representation, which is + * either a {@code Map}, {@code List}, {@code String}, {@code Double}, {@code + * Boolean}, or {@code null}. + */ + private static Value convertToValue(Object rawObject) { + Value.Builder valueBuilder = Value.newBuilder(); + if (rawObject == null) { + valueBuilder.setNullValue(NullValue.NULL_VALUE); + } else if (rawObject instanceof Double) { + valueBuilder.setNumberValue((Double) rawObject); + } else if (rawObject instanceof String) { + valueBuilder.setStringValue((String) rawObject); + } else if (rawObject instanceof Boolean) { + valueBuilder.setBoolValue((Boolean) rawObject); + } else if (rawObject instanceof Map) { + Struct.Builder structBuilder = Struct.newBuilder(); + @SuppressWarnings("unchecked") + Map map = (Map) rawObject; + for (Map.Entry entry : map.entrySet()) { + structBuilder.putFields(entry.getKey(), convertToValue(entry.getValue())); + } + valueBuilder.setStructValue(structBuilder); + } else if (rawObject instanceof List) { + ListValue.Builder listBuilder = ListValue.newBuilder(); + List list = (List) rawObject; + for (Object obj : list) { + listBuilder.addValues(convertToValue(obj)); + } + valueBuilder.setListValue(listBuilder); + } + return valueBuilder.build(); + } + + /** + * See corresponding Envoy proto message {@link io.envoyproxy.envoy.config.core.v3.Address}. + */ + static final class Address { + private final String address; + private final int port; + + Address(String address, int port) { + this.address = checkNotNull(address, "address"); + this.port = port; + } + + io.envoyproxy.envoy.config.core.v3.Address toEnvoyProtoAddress() { + return + io.envoyproxy.envoy.config.core.v3.Address.newBuilder().setSocketAddress( + io.envoyproxy.envoy.config.core.v3.SocketAddress.newBuilder().setAddress(address) + .setPortValue(port)).build(); + } + + io.envoyproxy.envoy.api.v2.core.Address toEnvoyProtoAddressV2() { + return + io.envoyproxy.envoy.api.v2.core.Address.newBuilder().setSocketAddress( + io.envoyproxy.envoy.api.v2.core.SocketAddress.newBuilder().setAddress(address) + .setPortValue(port)).build(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("address", address) + .add("port", port) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Address address1 = (Address) o; + return port == address1.port && Objects.equals(address, address1.address); + } + + @Override + public int hashCode() { + return Objects.hash(address, port); + } + } + + /** + * See corresponding Envoy proto message {@link io.envoyproxy.envoy.config.core.v3.Locality}. */ static final class Locality { private final String region; private final String zone; private final String subZone; - Locality(String region, String zone, String subZone) { - this.region = region; - this.zone = zone; - this.subZone = subZone; + Locality(@Nullable String region, @Nullable String zone, @Nullable String subZone) { + this.region = region == null ? "" : region; + this.zone = zone == null ? "" : zone; + this.subZone = subZone == null ? "" : subZone; } static Locality fromEnvoyProtoLocality(io.envoyproxy.envoy.config.core.v3.Locality locality) { @@ -163,7 +498,15 @@ static Locality fromEnvoyProtoLocalityV2(io.envoyproxy.envoy.api.v2.core.Localit /* subZone = */ locality.getSubZone()); } - io.envoyproxy.envoy.api.v2.core.Locality toEnvoyProtoLocality() { + io.envoyproxy.envoy.config.core.v3.Locality toEnvoyProtoLocality() { + return io.envoyproxy.envoy.config.core.v3.Locality.newBuilder() + .setRegion(region) + .setZone(zone) + .setSubZone(subZone) + .build(); + } + + io.envoyproxy.envoy.api.v2.core.Locality toEnvoyProtoLocalityV2() { return io.envoyproxy.envoy.api.v2.core.Locality.newBuilder() .setRegion(region) .setZone(zone) @@ -726,6 +1069,7 @@ static StructOrError convertEnvoyProtoHeaderMatcher( /** See corresponding Envoy proto message {@link io.envoyproxy.envoy.api.v2.route.RouteAction}. */ static final class RouteAction { + private final long timeoutNano; // Exactly one of the following fields is non-null. @Nullable private final String cluster; @@ -733,11 +1077,20 @@ static final class RouteAction { private final List weightedClusters; @VisibleForTesting - RouteAction(@Nullable String cluster, @Nullable List weightedClusters) { + RouteAction( + long timeoutNano, + @Nullable String cluster, + @Nullable List weightedClusters) { + this.timeoutNano = timeoutNano; this.cluster = cluster; this.weightedClusters = weightedClusters; } + + Long getTimeoutNano() { + return timeoutNano; + } + @Nullable String getCluster() { return cluster; @@ -757,18 +1110,20 @@ public boolean equals(Object o) { return false; } RouteAction that = (RouteAction) o; - return Objects.equals(cluster, that.cluster) + return Objects.equals(timeoutNano, that.timeoutNano) + && Objects.equals(cluster, that.cluster) && Objects.equals(weightedClusters, that.weightedClusters); } @Override public int hashCode() { - return Objects.hash(cluster, weightedClusters); + return Objects.hash(timeoutNano, cluster, weightedClusters); } @Override public String toString() { ToStringHelper toStringHelper = MoreObjects.toStringHelper(this); + toStringHelper.add("timeout", timeoutNano + "ns"); if (cluster != null) { toStringHelper.add("cluster", cluster); } @@ -805,7 +1160,16 @@ static StructOrError fromEnvoyProtoRouteAction( return StructOrError.fromError( "Unknown cluster specifier: " + proto.getClusterSpecifierCase()); } - return StructOrError.fromStruct(new RouteAction(cluster, weightedClusters)); + long timeoutNano = TimeUnit.SECONDS.toNanos(15L); // default 15s + if (proto.hasMaxGrpcTimeout()) { + timeoutNano = Durations.toNanos(proto.getMaxGrpcTimeout()); + } else if (proto.hasTimeout()) { + timeoutNano = Durations.toNanos(proto.getTimeout()); + } + if (timeoutNano == 0) { + timeoutNano = Long.MAX_VALUE; + } + return StructOrError.fromStruct(new RouteAction(timeoutNano, cluster, weightedClusters)); } } diff --git a/xds/src/main/java/io/grpc/xds/LoadReportClient.java b/xds/src/main/java/io/grpc/xds/LoadReportClient.java index 525b645d610..84d2b340cbd 100644 --- a/xds/src/main/java/io/grpc/xds/LoadReportClient.java +++ b/xds/src/main/java/io/grpc/xds/LoadReportClient.java @@ -27,7 +27,6 @@ import com.google.protobuf.Value; import com.google.protobuf.util.Durations; import io.envoyproxy.envoy.api.v2.core.Node; -import io.envoyproxy.envoy.api.v2.endpoint.ClusterStats; import io.envoyproxy.envoy.service.load_stats.v2.LoadReportingServiceGrpc; import io.envoyproxy.envoy.service.load_stats.v2.LoadStatsRequest; import io.envoyproxy.envoy.service.load_stats.v2.LoadStatsResponse; @@ -39,10 +38,7 @@ import io.grpc.internal.BackoffPolicy; import io.grpc.stub.StreamObserver; import io.grpc.xds.XdsLogger.XdsLogLevel; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.List; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; @@ -57,6 +53,7 @@ final class LoadReportClient { @VisibleForTesting static final String TARGET_NAME_METADATA_KEY = "PROXYLESS_CLIENT_HOSTNAME"; + private final InternalLogId logId; private final XdsLogger logger; private final ManagedChannel channel; private final Node node; @@ -65,9 +62,8 @@ final class LoadReportClient { private final Supplier stopwatchSupplier; private final Stopwatch retryStopwatch; private final BackoffPolicy.Provider backoffPolicyProvider; + private final LoadStatsManager loadStatsManager; - // Sources of load stats data for each cluster:cluster_service. - private final Map> loadStatsStoreMap = new HashMap<>(); private boolean started; @Nullable @@ -76,18 +72,17 @@ final class LoadReportClient { private ScheduledHandle lrsRpcRetryTimer; @Nullable private LrsStream lrsStream; - @Nullable - private LoadReportCallback callback; LoadReportClient( - InternalLogId logId, String targetName, + LoadStatsManager loadStatsManager, ManagedChannel channel, Node node, SynchronizationContext syncContext, ScheduledExecutorService scheduledExecutorService, BackoffPolicy.Provider backoffPolicyProvider, Supplier stopwatchSupplier) { + this.loadStatsManager = checkNotNull(loadStatsManager, "loadStatsManager"); this.channel = checkNotNull(channel, "channel"); this.syncContext = checkNotNull(syncContext, "syncContext"); this.timerService = checkNotNull(scheduledExecutorService, "timeService"); @@ -104,8 +99,9 @@ final class LoadReportClient { Value.newBuilder().setStringValue(targetName).build()) .build(); this.node = node.toBuilder().setMetadata(metadata).build(); - String logPrefix = checkNotNull(logId, "logId").toString().concat("-lrs-client"); - logger = XdsLogger.withPrefix(logPrefix); + logId = InternalLogId.allocate("lrs-client", targetName); + logger = XdsLogger.withLogId(logId); + logger.log(XdsLogLevel.INFO, "Created"); } /** @@ -113,12 +109,12 @@ final class LoadReportClient { * stats periodically. Calling this method on an already started {@link LoadReportClient} is * no-op. */ - void startLoadReporting(LoadReportCallback callback) { + void startLoadReporting() { if (started) { return; } - this.callback = callback; started = true; + logger.log(XdsLogLevel.INFO, "Starting load reporting RPC"); startLrsRpc(); } @@ -130,6 +126,7 @@ void stopLoadReporting() { if (!started) { return; } + logger.log(XdsLogLevel.INFO, "Stopping load reporting RPC"); if (lrsRpcRetryTimer != null) { lrsRpcRetryTimer.cancel(); } @@ -140,49 +137,6 @@ void stopLoadReporting() { // Do not shutdown channel as it is not owned by LrsClient. } - /** - * Provides this LoadReportClient source of load stats data for the given - * cluster:cluster_service. If requested, data from the given loadStatsStore is - * periodically queried and sent to traffic director by this LoadReportClient. - */ - void addLoadStatsStore( - String clusterName, @Nullable String clusterServiceName, LoadStatsStore loadStatsStore) { - checkState( - !loadStatsStoreMap.containsKey(clusterName) - || !loadStatsStoreMap.get(clusterName).containsKey(clusterServiceName), - "load stats for cluster: %s, cluster service: %s already exists", - clusterName, clusterServiceName); - logger.log( - XdsLogLevel.INFO, - "Add load stats for cluster: {0}, cluster_service: {1}", clusterName, clusterServiceName); - if (!loadStatsStoreMap.containsKey(clusterName)) { - loadStatsStoreMap.put(clusterName, new HashMap()); - } - Map clusterLoadStatsStores = loadStatsStoreMap.get(clusterName); - clusterLoadStatsStores.put(clusterServiceName, loadStatsStore); - } - - /** - * Stops providing load stats data for the given cluster:cluster_service. - */ - void removeLoadStatsStore(String clusterName, @Nullable String clusterServiceName) { - checkState( - loadStatsStoreMap.containsKey(clusterName) - && loadStatsStoreMap.get(clusterName).containsKey(clusterServiceName), - "load stats for cluster: %s, cluster service: %s does not exist", - clusterName, clusterServiceName); - logger.log( - XdsLogLevel.INFO, - "Remove load stats for cluster: {0}, cluster_service: {1}", - clusterName, - clusterServiceName); - Map clusterLoadStatsStores = loadStatsStoreMap.get(clusterName); - clusterLoadStatsStores.remove(clusterServiceName); - if (clusterLoadStatsStores.isEmpty()) { - loadStatsStoreMap.remove(clusterName); - } - } - @VisibleForTesting static class LoadReportingTask implements Runnable { private final LrsStream stream; @@ -217,27 +171,21 @@ private void startLrsRpc() { private class LrsStream implements StreamObserver { - // Cluster to report loads for asked by management server. - final Set clusterNames = new HashSet<>(); final LoadReportingServiceGrpc.LoadReportingServiceStub stub; - final Stopwatch reportStopwatch; StreamObserver lrsRequestWriter; boolean initialResponseReceived; boolean closed; long loadReportIntervalNano = -1; + boolean reportAllClusters; + List clusterNames; // clusters to report loads for, if not report all. ScheduledHandle loadReportTimer; LrsStream(LoadReportingServiceGrpc.LoadReportingServiceStub stub, Stopwatch stopwatch) { this.stub = checkNotNull(stub, "stub"); - reportStopwatch = checkNotNull(stopwatch, "stopwatch"); } void start() { lrsRequestWriter = stub.withWaitForReady().streamLoadStats(this); - reportStopwatch.reset().start(); - - // Send an initial LRS request with empty cluster stats. Management server is able to - // infer clusters the gRPC client sending loads to. LoadStatsRequest initRequest = LoadStatsRequest.newBuilder() .setNode(node) @@ -278,20 +226,12 @@ public void run() { } private void sendLoadReport() { - long interval = reportStopwatch.elapsed(TimeUnit.NANOSECONDS); - reportStopwatch.reset().start(); LoadStatsRequest.Builder requestBuilder = LoadStatsRequest.newBuilder().setNode(node); - for (String name : clusterNames) { - if (loadStatsStoreMap.containsKey(name)) { - Map clusterLoadStatsStores = loadStatsStoreMap.get(name); - for (LoadStatsStore statsStore : clusterLoadStatsStores.values()) { - ClusterStats report = - statsStore.generateLoadReport() - .toBuilder() - .setLoadReportInterval(Durations.fromNanos(interval)) - .build(); - requestBuilder.addClusterStats(report); - } + if (reportAllClusters) { + requestBuilder.addAllClusterStats(loadStatsManager.getAllLoadReports()); + } else { + for (String name : clusterNames) { + requestBuilder.addAllClusterStats(loadStatsManager.getClusterLoadReports(name)); } } LoadStatsRequest request = requestBuilder.build(); @@ -317,27 +257,22 @@ private void handleResponse(LoadStatsResponse response) { if (closed) { return; } - if (!initialResponseReceived) { logger.log(XdsLogLevel.DEBUG, "Received LRS initial response:\n{0}", response); initialResponseReceived = true; } else { logger.log(XdsLogLevel.DEBUG, "Received LRS response:\n{0}", response); } - long interval = Durations.toNanos(response.getLoadReportingInterval()); - if (interval != loadReportIntervalNano) { - logger.log(XdsLogLevel.INFO, "Update load reporting interval to {0} ns", interval); - loadReportIntervalNano = interval; - callback.onReportResponse(loadReportIntervalNano); - } - if (clusterNames.size() != response.getClustersCount() - || !clusterNames.containsAll(response.getClustersList())) { - logger.log( - XdsLogLevel.INFO, - "Update load reporting clusters to {0}", response.getClustersList()); - clusterNames.clear(); - clusterNames.addAll(response.getClustersList()); + reportAllClusters = response.getSendAllClusters(); + if (reportAllClusters) { + logger.log(XdsLogLevel.INFO, "Report loads for all clusters"); + } else { + logger.log(XdsLogLevel.INFO, "Report loads for clusters: ", response.getClustersList()); + clusterNames = response.getClustersList(); } + long interval = Durations.toNanos(response.getLoadReportingInterval()); + logger.log(XdsLogLevel.INFO, "Update load reporting interval to {0} ns", interval); + loadReportIntervalNano = interval; scheduleNextLoadReport(); } @@ -400,21 +335,4 @@ private void cleanUp() { } } } - - /** - * Callbacks for passing information received from client load reporting responses to xDS load - * balancer, such as the load reporting interval requested by the traffic director. - * - *

Implementations are not required to be thread-safe as callbacks will be invoked in xDS load - * balancer's {@link io.grpc.SynchronizationContext}. - */ - interface LoadReportCallback { - - /** - * The load reporting interval has been received. - * - * @param reportIntervalNano load reporting interval requested by remote traffic director. - */ - void onReportResponse(long reportIntervalNano); - } } diff --git a/xds/src/main/java/io/grpc/xds/LoadStatsManager.java b/xds/src/main/java/io/grpc/xds/LoadStatsManager.java new file mode 100644 index 00000000000..5372d37b0ea --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/LoadStatsManager.java @@ -0,0 +1,164 @@ +/* + * 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.checkState; + +import com.google.common.annotations.VisibleForTesting; +import io.envoyproxy.envoy.api.v2.endpoint.ClusterStats; +import io.grpc.xds.EnvoyProtoData.Locality; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * Manages all stats for client side load. + */ +final class LoadStatsManager { + private final LoadStatsStoreFactory loadStatsStoreFactory; + private final Map>> loadStatsStores + = new HashMap<>(); + + LoadStatsManager() { + this(LoadStatsStoreImpl.getDefaultFactory()); + } + + @VisibleForTesting + LoadStatsManager(LoadStatsStoreFactory factory) { + this.loadStatsStoreFactory = factory; + } + + /** + * Adds and retrieves the stats object for tracking loads for the given cluster:cluster_service. + * The returned {@link LoadStatsStore} is reference-counted, caller should use + * {@link #removeLoadStats} to release the reference when it is no longer used. + */ + LoadStatsStore addLoadStats(String cluster, @Nullable String clusterService) { + if (!loadStatsStores.containsKey(cluster)) { + loadStatsStores.put(cluster, new HashMap>()); + } + Map> clusterLoadStatsStores + = loadStatsStores.get(cluster); + if (!clusterLoadStatsStores.containsKey(clusterService)) { + clusterLoadStatsStores.put( + clusterService, + ReferenceCounted.wrap(loadStatsStoreFactory.newLoadStatsStore(cluster, clusterService))); + } + ReferenceCounted ref = clusterLoadStatsStores.get(clusterService); + ref.retain(); + return ref.get(); + } + + /** + * Discards stats object used for tracking loads for the given cluster:cluster_service. + */ + void removeLoadStats(String cluster, @Nullable String clusterService) { + checkState( + loadStatsStores.containsKey(cluster) + && loadStatsStores.get(cluster).containsKey(clusterService), + "stats for cluster %s, cluster service %s not exits"); + Map> clusterLoadStatsStores = + loadStatsStores.get(cluster); + ReferenceCounted ref = clusterLoadStatsStores.get(clusterService); + ref.release(); + if (ref.getReferenceCount() == 0) { + clusterLoadStatsStores.remove(clusterService); + } + if (clusterLoadStatsStores.isEmpty()) { + loadStatsStores.remove(cluster); + } + } + + /** + * Generates reports summarizing the stats recorded for loads sent to the given cluster for + * the interval between calls of this method or {@link #getAllLoadReports}. A cluster may send + * loads to more than one cluster_service, they are included in separate stats reports. + */ + // TODO(chengyuanzhang): do not use proto type directly. + List getClusterLoadReports(String cluster) { + List res = new ArrayList<>(); + Map> clusterLoadStatsStores = + loadStatsStores.get(cluster); + if (clusterLoadStatsStores == null) { + return res; + } + for (ReferenceCounted ref : clusterLoadStatsStores.values()) { + res.add(ref.get().generateLoadReport()); + } + return res; + } + + /** + * Generates reports summarized the stats recorded for loads sent to all clusters for the + * interval between calls of this method or {@link #getClusterLoadReports}. Each report + * includes stats for one cluster:cluster_service. + */ + // TODO(chengyuanzhang): do not use proto type directly. + List getAllLoadReports() { + List res = new ArrayList<>(); + for (Map> clusterLoadStatsStores + : loadStatsStores.values()) { + for (ReferenceCounted ref : clusterLoadStatsStores.values()) { + res.add(ref.get().generateLoadReport()); + } + } + return res; + } + + @VisibleForTesting + interface LoadStatsStoreFactory { + LoadStatsStore newLoadStatsStore(String cluster, String clusterService); + } + + /** + * Interface for client side load stats store. An {@code LoadStatsStore} maintains load stats per + * cluster:cluster_service exposed by traffic director from a gRPC client's perspective, + * including dropped calls. Load stats for endpoints are aggregated in locality granularity + * while the numbers of dropped calls are aggregated in cluster:cluster_service granularity. + */ + interface LoadStatsStore { + + /** + * Generates a report based on recorded load stats (including RPC counts, backend metrics and + * dropped calls) for the interval since the previous call of this method. + */ + // TODO(chengyuanzhang): do not use proto type directly. + ClusterStats generateLoadReport(); + + /** + * Track load stats for endpoints in the provided locality. Only load stats for endpoints + * in tracked localities will be included in generated load reports. + */ + ClientLoadCounter addLocality(Locality locality); + + /** + * Drop tracking load stats for endpoints in the provided locality. Load stats for endpoints + * in removed localities will no longer be included in future generated load reports after + * their currently recording stats have been fully reported. + */ + void removeLocality(Locality locality); + + /** + * Records a drop decision. + * + *

This method is thread-safe. + */ + void recordDroppedRequest(String category); + } +} diff --git a/xds/src/main/java/io/grpc/xds/LoadStatsStore.java b/xds/src/main/java/io/grpc/xds/LoadStatsStore.java deleted file mode 100644 index cd76be41edf..00000000000 --- a/xds/src/main/java/io/grpc/xds/LoadStatsStore.java +++ /dev/null @@ -1,107 +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 io.envoyproxy.envoy.api.v2.endpoint.ClusterStats; -import io.grpc.xds.EnvoyProtoData.Locality; -import javax.annotation.Nullable; - -/** - * Interface for client side load stats store. An {@code LoadStatsStore} maintains load stats per - * cluster:cluster_service exposed by traffic director from a gRPC client's perspective, - * including dropped calls. Load stats for endpoints (i.e., Google backends) are aggregated in - * locality granularity (i.e., Google cluster) while the numbers of dropped calls are aggregated - * in cluster:cluster_service granularity. - * - *

An {@code LoadStatsStore} only tracks loads for localities exposed by remote traffic - * director. A proper usage should be - * - *

    - *
  1. Let {@link LoadStatsStore} track the locality newly exposed by traffic director by - * calling {@link #addLocality(Locality)}. - *
  2. Use the locality counter returned by {@link #getLocalityCounter(Locality)} to record - * load stats for the corresponding locality. - *
  3. Tell {@link LoadStatsStore} to stop tracking the locality no longer exposed by traffic - * director by calling {@link #removeLocality(Locality)}. - *
- * - *

No locality information is needed for recording dropped calls since they are aggregated in - * cluster granularity. - */ -interface LoadStatsStore { - - /** - * Generates a {@link ClusterStats} proto message as the load report based on recorded load stats - * (including RPC * counts, backend metrics and dropped calls) for the interval since the previous - * call of this method. - * - *

Loads for localities no longer under tracking will not be included in generated load reports - * once all of theirs loads are completed and reported. - * - *

The fields {@code cluster_name} and {@code load_report_interval} in the returned {@link - * ClusterStats} needs to be set before it is ready to be sent to the traffic director for load - * reporting. - * - *

This method is not thread-safe and should be called from the same synchronized context - * used by {@link LoadReportClient}. - */ - ClusterStats generateLoadReport(); - - /** - * Starts tracking load stats for endpoints in the provided locality. Only load stats for - * endpoints in added localities will be recorded and included in generated load reports. - * - *

This method needs to be called at locality updates only for newly assigned localities in - * endpoint discovery responses before recording loads for those localities. - * - *

This method is not thread-safe and should be called from the same synchronized context - * used by {@link LoadReportClient}. - */ - void addLocality(Locality locality); - - /** - * Stops tracking load stats for endpoints in the provided locality. gRPC clients are expected not - * to send loads to localities no longer exposed by traffic director. Load stats for endpoints in - * removed localities will no longer be included in future generated load reports after their - * recorded and ongoing loads have been reported. - * - *

This method needs to be called at locality updates only for newly removed localities. - * Forgetting calling this method for localities no longer under track will result in memory - * waste and keep including zero-load upstream locality stats in generated load reports. - * - *

This method is not thread-safe and should be called from the same synchronized context - * used by {@link LoadReportClient}. - */ - void removeLocality(Locality locality); - - /** - * Returns the locality counter that does locality level stats aggregation for the provided - * locality. If the provided locality is not tracked, {@code null} will be returned. - * - *

This method is thread-safe. - */ - @Nullable - ClientLoadCounter getLocalityCounter(Locality locality); - - /** - * Records a drop decision made by a {@link io.grpc.LoadBalancer.SubchannelPicker} instance - * with the provided category. Drops are aggregated in cluster granularity. - * - *

This method is thread-safe. - */ - void recordDroppedRequest(String category); -} diff --git a/xds/src/main/java/io/grpc/xds/LoadStatsStoreImpl.java b/xds/src/main/java/io/grpc/xds/LoadStatsStoreImpl.java index 37ff09d91be..ad597d5484c 100644 --- a/xds/src/main/java/io/grpc/xds/LoadStatsStoreImpl.java +++ b/xds/src/main/java/io/grpc/xds/LoadStatsStoreImpl.java @@ -17,19 +17,24 @@ package io.grpc.xds; 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.protobuf.util.Durations; import io.envoyproxy.envoy.api.v2.endpoint.ClusterStats; import io.envoyproxy.envoy.api.v2.endpoint.ClusterStats.DroppedRequests; import io.envoyproxy.envoy.api.v2.endpoint.EndpointLoadMetricStats; import io.envoyproxy.envoy.api.v2.endpoint.UpstreamLocalityStats; +import io.grpc.internal.GrpcUtil; import io.grpc.xds.ClientLoadCounter.ClientLoadSnapshot; import io.grpc.xds.ClientLoadCounter.MetricValue; import io.grpc.xds.EnvoyProtoData.Locality; +import io.grpc.xds.LoadStatsManager.LoadStatsStore; +import io.grpc.xds.LoadStatsManager.LoadStatsStoreFactory; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; @@ -45,12 +50,14 @@ final class LoadStatsStoreImpl implements LoadStatsStore { @Nullable @SuppressWarnings("unused") private final String clusterServiceName; - private final ConcurrentMap localityLoadCounters; + private final ConcurrentMap> localityLoadCounters + = new ConcurrentHashMap<>(); // Cluster level dropped request counts for each category decision made by xDS load balancer. private final ConcurrentMap dropCounters; + private final Stopwatch stopwatch; LoadStatsStoreImpl(String clusterName, @Nullable String clusterServiceName) { - this(clusterName, clusterServiceName, new ConcurrentHashMap(), + this(clusterName, clusterServiceName, GrpcUtil.STOPWATCH_SUPPLIER.get(), new ConcurrentHashMap()); } @@ -58,12 +65,13 @@ final class LoadStatsStoreImpl implements LoadStatsStore { LoadStatsStoreImpl( String clusterName, @Nullable String clusterServiceName, - ConcurrentMap localityLoadCounters, + Stopwatch stopwatch, ConcurrentMap dropCounters) { this.clusterName = checkNotNull(clusterName, "clusterName"); this.clusterServiceName = clusterServiceName; - this.localityLoadCounters = checkNotNull(localityLoadCounters, "localityLoadCounters"); + this.stopwatch = checkNotNull(stopwatch, "stopwatch"); this.dropCounters = checkNotNull(dropCounters, "dropCounters"); + stopwatch.reset().start(); } @Override @@ -71,10 +79,11 @@ public ClusterStats generateLoadReport() { ClusterStats.Builder statsBuilder = ClusterStats.newBuilder(); statsBuilder.setClusterName(clusterName); // TODO(chengyuangzhang): also set cluster_service_name if provided. - for (Map.Entry entry : localityLoadCounters.entrySet()) { - ClientLoadSnapshot snapshot = entry.getValue().snapshot(); + for (Map.Entry> entry + : localityLoadCounters.entrySet()) { + ClientLoadSnapshot snapshot = entry.getValue().get().snapshot(); UpstreamLocalityStats.Builder localityStatsBuilder = - UpstreamLocalityStats.newBuilder().setLocality(entry.getKey().toEnvoyProtoLocality()); + UpstreamLocalityStats.newBuilder().setLocality(entry.getKey().toEnvoyProtoLocalityV2()); localityStatsBuilder .setTotalSuccessfulRequests(snapshot.getCallsSucceeded()) .setTotalErrorRequests(snapshot.getCallsFailed()) @@ -90,7 +99,7 @@ public ClusterStats generateLoadReport() { statsBuilder.addUpstreamLocalityStats(localityStatsBuilder); // Discard counters for localities that are no longer exposed by the remote balancer and // no RPCs ongoing. - if (!entry.getValue().isActive() && snapshot.getCallsInProgress() == 0) { + if (entry.getValue().getReferenceCount() == 0 && snapshot.getCallsInProgress() == 0) { localityLoadCounters.remove(entry.getKey()); } } @@ -103,32 +112,27 @@ public ClusterStats generateLoadReport() { .setDroppedCount(drops)); } statsBuilder.setTotalDroppedRequests(totalDrops); + statsBuilder.setLoadReportInterval( + Durations.fromNanos(stopwatch.elapsed(TimeUnit.NANOSECONDS))); + stopwatch.reset().start(); return statsBuilder.build(); } @Override - public void addLocality(final Locality locality) { - ClientLoadCounter counter = localityLoadCounters.get(locality); - checkState(counter == null || !counter.isActive(), - "An active counter for locality %s already exists", locality); + public ClientLoadCounter addLocality(final Locality locality) { + ReferenceCounted counter = localityLoadCounters.get(locality); if (counter == null) { - localityLoadCounters.put(locality, new ClientLoadCounter()); - } else { - counter.setActive(true); + counter = ReferenceCounted.wrap(new ClientLoadCounter()); + localityLoadCounters.put(locality, counter); } + counter.retain(); + return counter.get(); } @Override public void removeLocality(final Locality locality) { - ClientLoadCounter counter = localityLoadCounters.get(locality); - checkState(counter != null && counter.isActive(), - "No active counter for locality %s exists", locality); - counter.setActive(false); - } - - @Override - public ClientLoadCounter getLocalityCounter(final Locality locality) { - return localityLoadCounters.get(locality); + ReferenceCounted counter = localityLoadCounters.get(locality); + counter.release(); } @Override @@ -142,4 +146,13 @@ public void recordDroppedRequest(String category) { } counter.getAndIncrement(); } + + static LoadStatsStoreFactory getDefaultFactory() { + return new LoadStatsStoreFactory() { + @Override + public LoadStatsStore newLoadStatsStore(String cluster, String clusterService) { + return new LoadStatsStoreImpl(cluster, clusterService); + } + }; + } } diff --git a/xds/src/main/java/io/grpc/xds/LocalityStore.java b/xds/src/main/java/io/grpc/xds/LocalityStore.java index 183b4bbb7f5..4fa507e6a12 100644 --- a/xds/src/main/java/io/grpc/xds/LocalityStore.java +++ b/xds/src/main/java/io/grpc/xds/LocalityStore.java @@ -47,6 +47,7 @@ import io.grpc.xds.EnvoyProtoData.LbEndpoint; import io.grpc.xds.EnvoyProtoData.Locality; import io.grpc.xds.EnvoyProtoData.LocalityLbEndpoints; +import io.grpc.xds.LoadStatsManager.LoadStatsStore; import io.grpc.xds.OrcaOobUtil.OrcaReportingConfig; import io.grpc.xds.OrcaOobUtil.OrcaReportingHelperWrapper; import io.grpc.xds.WeightedRandomPicker.WeightedChildPicker; @@ -311,8 +312,8 @@ private final class LocalityLbInfo { LocalityLbInfo(Locality locality) { this.locality = checkNotNull(locality, "locality"); - loadStatsStore.addLocality(locality); - childHelper = new ChildHelper(); + ClientLoadCounter counter = loadStatsStore.addLocality(locality); + childHelper = new ChildHelper(counter); childBalancer = loadBalancerProvider.newLoadBalancer(childHelper); } @@ -368,8 +369,7 @@ class ChildHelper extends ForwardingLoadBalancerHelper { private SubchannelPicker currentChildPicker = XdsSubchannelPickers.BUFFER_PICKER; private ConnectivityState currentChildState = CONNECTING; - ChildHelper() { - final ClientLoadCounter counter = loadStatsStore.getLocalityCounter(locality); + ChildHelper(final ClientLoadCounter counter) { Helper delegate = new ForwardingLoadBalancerHelper() { @Override protected Helper delegate() { diff --git a/xds/src/main/java/io/grpc/xds/LrsLoadBalancer.java b/xds/src/main/java/io/grpc/xds/LrsLoadBalancer.java index 55ca451271b..edcfcc0cc39 100644 --- a/xds/src/main/java/io/grpc/xds/LrsLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/LrsLoadBalancer.java @@ -26,6 +26,7 @@ import io.grpc.util.GracefulSwitchLoadBalancer; import io.grpc.xds.ClientLoadCounter.LoadRecordingSubchannelPicker; import io.grpc.xds.EnvoyProtoData.Locality; +import io.grpc.xds.LoadStatsManager.LoadStatsStore; import io.grpc.xds.LrsLoadBalancerProvider.LrsConfig; import io.grpc.xds.XdsSubchannelPickers.ErrorPicker; import java.util.Objects; @@ -58,8 +59,7 @@ public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) { checkAndSetUp(config, store); if (switchingLoadBalancer == null) { - loadStatsStore.addLocality(config.locality); - final ClientLoadCounter counter = loadStatsStore.getLocalityCounter(config.locality); + final ClientLoadCounter counter = loadStatsStore.addLocality(config.locality); LoadBalancer.Helper loadRecordingHelper = new ForwardingLoadBalancerHelper() { @Override protected Helper delegate() { diff --git a/xds/src/main/java/io/grpc/xds/ReferenceCounted.java b/xds/src/main/java/io/grpc/xds/ReferenceCounted.java new file mode 100644 index 00000000000..b62340ffa0a --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/ReferenceCounted.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020 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.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * A reference count wrapper for objects. This class does not take the ownership for the object, + * but only provides usage counting. The real owner of the wrapped object is responsible for + * managing the lifecycle of the object. + * + *

Intended for a container class to keep track of lifecycle for elements it contains. This + * wrapper itself should never be returned to the consumers of the elements to avoid reference + * counts being leaked. + */ +final class ReferenceCounted { + private final T instance; + private int refs; + + private ReferenceCounted(T instance) { + this.instance = instance; + } + + static ReferenceCounted wrap(T instance) { + checkNotNull(instance, "instance"); + return new ReferenceCounted<>(instance); + } + + void retain() { + refs++; + } + + void release() { + checkState(refs > 0, "reference reached 0"); + refs--; + } + + int getReferenceCount() { + return refs; + } + + T get() { + return instance; + } +} diff --git a/xds/src/main/java/io/grpc/xds/XdsAttributes.java b/xds/src/main/java/io/grpc/xds/XdsAttributes.java index 662c9a69a5a..57796658636 100644 --- a/xds/src/main/java/io/grpc/xds/XdsAttributes.java +++ b/xds/src/main/java/io/grpc/xds/XdsAttributes.java @@ -22,6 +22,7 @@ import io.grpc.NameResolver; import io.grpc.internal.ObjectPool; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; +import io.grpc.xds.LoadStatsManager.LoadStatsStore; /** * Special attributes that are only useful to gRPC in the XDS context. diff --git a/xds/src/main/java/io/grpc/xds/XdsClient.java b/xds/src/main/java/io/grpc/xds/XdsClient.java index b74ec3360b3..69a12843003 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClient.java +++ b/xds/src/main/java/io/grpc/xds/XdsClient.java @@ -37,6 +37,7 @@ import io.grpc.xds.EnvoyProtoData.Route; import io.grpc.xds.EnvoyServerProtoData.Listener; 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; @@ -518,21 +519,34 @@ void watchListenerData(int port, ListenerWatcher watcher) { } /** - * Report client load stats to a remote server for the given cluster:cluster_service. - * - *

Note: currently we can only report loads for a single cluster:cluster_service, - * as the design for adding clusters to report loads for while load reporting is - * happening is undefined. + * Starts client side load reporting via LRS. All clusters report load through one LRS stream, + * only the first call of this method effectively starts the LRS stream. + */ + void reportClientStats() { + } + + /** + * Stops client side load reporting via LRS. All clusters report load through one LRS stream, + * only the last call of this method effectively stops the LRS stream. + */ + void cancelClientStatsReport() { + } + + /** + * Starts recording client load stats for the given cluster:cluster_service. Caller should use + * the returned {@link LoadStatsStore} to record and aggregate stats for load sent to the given + * cluster:cluster_service. Recorded stats may be reported to a load reporting server if enabled. */ - void reportClientStats( - String clusterName, @Nullable String clusterServiceName, LoadStatsStore loadStatsStore) { + LoadStatsStore addClientStats(String clusterName, @Nullable String clusterServiceName) { throw new UnsupportedOperationException(); } /** - * Stops reporting client load stats to the remote server for the given cluster:cluster_service. + * Stops recording client load stats for the given cluster:cluster_service. The load reporting + * server will no longer receive stats for the given cluster:cluster_service after this call. */ - void cancelClientStatsReport(String clusterName, @Nullable String clusterServiceName) { + void removeClientStats(String clusterName, @Nullable String clusterServiceName) { + throw new UnsupportedOperationException(); } abstract static class XdsClientFactory { @@ -601,13 +615,17 @@ public synchronized XdsClient returnObject(Object object) { * Factory for creating channels to xDS severs. */ abstract static class XdsChannelFactory { - private static final XdsChannelFactory DEFAULT_INSTANCE = new XdsChannelFactory() { + @VisibleForTesting + static boolean experimentalV3SupportEnvVar = Boolean.parseBoolean( + System.getenv("GRPC_XDS_EXPERIMENTAL_V3_SUPPORT")); + private static final String XDS_V3_SERVER_FEATURE = "xds_v3"; + private static final XdsChannelFactory DEFAULT_INSTANCE = new XdsChannelFactory() { /** * Creates a channel to the first server in the given list. */ @Override - ManagedChannel createChannel(List servers) { + XdsChannel createChannel(List servers) { checkArgument(!servers.isEmpty(), "No management server provided."); XdsLogger logger = XdsLogger.withPrefix("xds-client-channel-factory"); ServerInfo serverInfo = servers.get(0); @@ -629,9 +647,13 @@ ManagedChannel createChannel(List servers) { channelBuilder = ManagedChannelBuilder.forTarget(serverUri); } - return channelBuilder + ManagedChannel channel = channelBuilder .keepAliveTime(5, TimeUnit.MINUTES) .build(); + boolean useProtocolV3 = experimentalV3SupportEnvVar + && serverInfo.getServerFeatures().contains(XDS_V3_SERVER_FEATURE); + + return new XdsChannel(channel, useProtocolV3); } }; @@ -642,6 +664,25 @@ static XdsChannelFactory getInstance() { /** * Creates a channel to one of the provided management servers. */ - abstract ManagedChannel createChannel(List servers); + abstract XdsChannel createChannel(List servers); + } + + static final class XdsChannel { + private final ManagedChannel managedChannel; + private final boolean useProtocolV3; + + @VisibleForTesting + XdsChannel(ManagedChannel managedChannel, boolean useProtocolV3) { + this.managedChannel = managedChannel; + this.useProtocolV3 = useProtocolV3; + } + + ManagedChannel getManagedChannel() { + return managedChannel; + } + + boolean isUseProtocolV3() { + return useProtocolV3; + } } } diff --git a/xds/src/main/java/io/grpc/xds/XdsClientImpl.java b/xds/src/main/java/io/grpc/xds/XdsClientImpl.java index e8bd49906b3..c386daca3c6 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClientImpl.java +++ b/xds/src/main/java/io/grpc/xds/XdsClientImpl.java @@ -27,14 +27,8 @@ import com.google.protobuf.Any; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.MessageOrBuilder; -import com.google.protobuf.Struct; -import com.google.protobuf.Value; import com.google.protobuf.util.JsonFormat; import com.google.rpc.Code; -import io.envoyproxy.envoy.api.v2.DiscoveryRequest; -import io.envoyproxy.envoy.api.v2.DiscoveryResponse; -import io.envoyproxy.envoy.api.v2.core.Node; -import io.envoyproxy.envoy.api.v2.core.SocketAddress; 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; @@ -50,7 +44,9 @@ 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.v2.AggregatedDiscoveryServiceGrpc; +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.ManagedChannel; import io.grpc.Status; @@ -62,9 +58,10 @@ 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.LoadReportClient.LoadReportCallback; +import io.grpc.xds.LoadStatsManager.LoadStatsStore; import io.grpc.xds.XdsLogger.XdsLogLevel; import java.util.ArrayList; import java.util.Collection; @@ -87,12 +84,14 @@ final class XdsClientImpl extends XdsClient { @VisibleForTesting static final String ADS_TYPE_URL_LDS_V2 = "type.googleapis.com/envoy.api.v2.Listener"; - private static final String ADS_TYPE_URL_LDS = + @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"; - private static final String ADS_TYPE_URL_RDS = + @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" @@ -102,12 +101,14 @@ final class XdsClientImpl extends XdsClient { + ".HttpConnectionManager"; @VisibleForTesting static final String ADS_TYPE_URL_CDS_V2 = "type.googleapis.com/envoy.api.v2.Cluster"; - private static final String ADS_TYPE_URL_CDS = + @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"; - private static final String ADS_TYPE_URL_EDS = + @VisibleForTesting + static final String ADS_TYPE_URL_EDS = "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment"; private final MessagePrinter respPrinter = new MessagePrinter(); @@ -117,6 +118,7 @@ final class XdsClientImpl extends XdsClient { // Name of the target server this gRPC client is trying to talk to. private final String targetName; private final ManagedChannel channel; + private final boolean useProtocolV3; private final SynchronizationContext syncContext; private final ScheduledExecutorService timeService; private final BackoffPolicy.Provider backoffPolicyProvider; @@ -161,6 +163,8 @@ final class XdsClientImpl extends XdsClient { // Timers for concluding EDS resources not found. private final Map edsRespTimers = new HashMap<>(); + private final LoadStatsManager loadStatsManager = new LoadStatsManager(); + // Timer for concluding the currently requesting LDS resource not found. @Nullable private ScheduledHandle ldsRespTimer; @@ -170,13 +174,14 @@ final class XdsClientImpl extends XdsClient { private ScheduledHandle rdsRespTimer; @Nullable - private AdsStream adsStream; + 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. @@ -201,9 +206,11 @@ final class XdsClientImpl extends XdsClient { BackoffPolicy.Provider backoffPolicyProvider, Supplier stopwatchSupplier) { this.targetName = checkNotNull(targetName, "targetName"); - this.channel = + XdsChannel xdsChannel = checkNotNull(channelFactory, "channelFactory") .createChannel(checkNotNull(servers, "servers")); + this.channel = xdsChannel.getManagedChannel(); + this.useProtocolV3 = xdsChannel.isUseProtocolV3(); this.node = checkNotNull(node, "node"); this.syncContext = checkNotNull(syncContext, "syncContext"); this.timeService = checkNotNull(timeService, "timeService"); @@ -273,7 +280,7 @@ void watchConfigData(String targetAuthority, ConfigWatcher watcher) { if (adsStream == null) { startRpcStream(); } - adsStream.sendXdsRequest(ADS_TYPE_URL_LDS_V2, ImmutableList.of(ldsResourceName)); + adsStream.sendXdsRequest(ResourceType.LDS, ImmutableList.of(ldsResourceName)); ldsRespTimer = syncContext .schedule( @@ -314,7 +321,7 @@ void watchClusterData(String clusterName, ClusterWatcher watcher) { if (adsStream == null) { startRpcStream(); } - adsStream.sendXdsRequest(ADS_TYPE_URL_CDS_V2, clusterWatchers.keySet()); + adsStream.sendXdsRequest(ResourceType.CDS, clusterWatchers.keySet()); ScheduledHandle timeoutHandle = syncContext .schedule( @@ -351,7 +358,7 @@ void cancelClusterDataWatch(String clusterName, ClusterWatcher watcher) { } checkState(adsStream != null, "Severe bug: ADS stream was not created while an endpoint watcher was registered"); - adsStream.sendXdsRequest(ADS_TYPE_URL_CDS_V2, clusterWatchers.keySet()); + adsStream.sendXdsRequest(ResourceType.CDS, clusterWatchers.keySet()); } } @@ -392,7 +399,7 @@ void watchEndpointData(String clusterName, EndpointWatcher watcher) { if (adsStream == null) { startRpcStream(); } - adsStream.sendXdsRequest(ADS_TYPE_URL_EDS_V2, endpointWatchers.keySet()); + adsStream.sendXdsRequest(ResourceType.EDS, endpointWatchers.keySet()); ScheduledHandle timeoutHandle = syncContext .schedule( @@ -427,7 +434,7 @@ void cancelEndpointDataWatch(String clusterName, EndpointWatcher watcher) { // Currently in retry backoff. return; } - adsStream.sendXdsRequest(ADS_TYPE_URL_EDS_V2, endpointWatchers.keySet()); + adsStream.sendXdsRequest(ResourceType.EDS, endpointWatchers.keySet()); } } @@ -448,7 +455,7 @@ void watchListenerData(int port, ListenerWatcher watcher) { startRpcStream(); } updateNodeMetadataForListenerRequest(port); - adsStream.sendXdsRequest(ADS_TYPE_URL_LDS_V2, ImmutableList.of()); + adsStream.sendXdsRequest(ResourceType.LDS, ImmutableList.of()); ldsRespTimer = syncContext .schedule( @@ -458,58 +465,57 @@ void watchListenerData(int port, ListenerWatcher watcher) { /** In case of Listener watcher metadata to be updated to include port. */ private void updateNodeMetadataForListenerRequest(int port) { - Struct newMetadata = node.getMetadata().toBuilder() - .putFields("TRAFFICDIRECTOR_PROXYLESS", - Value.newBuilder().setStringValue("1").build()) - .build(); - io.envoyproxy.envoy.api.v2.core.Address listeningAddress = - io.envoyproxy.envoy.api.v2.core.Address.newBuilder() - .setSocketAddress( - SocketAddress.newBuilder() - .setAddress("0.0.0.0") - .setPortValue(port) - .build()) - .build(); + Map newMetadata = new HashMap<>(); + if (node.getMetadata() != null) { + newMetadata.putAll(node.getMetadata()); + } + newMetadata.put("TRAFFICDIRECTOR_PROXYLESS", "1"); + EnvoyProtoData.Address listeningAddress = + new EnvoyProtoData.Address("0.0.0.0", port); node = node.toBuilder().setMetadata(newMetadata).addListeningAddresses(listeningAddress).build(); } @Override - void reportClientStats( - String clusterName, @Nullable String clusterServiceName, LoadStatsStore loadStatsStore) { + void reportClientStats() { if (lrsClient == null) { + logger.log(XdsLogLevel.INFO, "Turning on load reporting"); lrsClient = new LoadReportClient( - logId, targetName, + loadStatsManager, channel, - node, + node.toEnvoyProtoNodeV2(), syncContext, timeService, backoffPolicyProvider, stopwatchSupplier); - lrsClient.startLoadReporting(new LoadReportCallback() { - @Override - public void onReportResponse(long reportIntervalNano) {} - }); } - logger.log( - XdsLogLevel.INFO, - "Report loads for cluster: {0}, cluster_service: {1}", clusterName, clusterServiceName); - lrsClient.addLoadStatsStore(clusterName, clusterServiceName, loadStatsStore); + if (loadReportCount == 0) { + lrsClient.startLoadReporting(); + } + loadReportCount++; } @Override - void cancelClientStatsReport(String clusterName, @Nullable String clusterServiceName) { - checkState(lrsClient != null, "load reporting was never started"); - logger.log( - XdsLogLevel.INFO, - "Stop reporting loads for cluster: {0}, cluster_service: {1}", - clusterName, - clusterServiceName); - lrsClient.removeLoadStatsStore(clusterName, clusterServiceName); - // TODO(chengyuanzhang): can be optimized to stop load reporting if no more loads need - // to be reported. + 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 @@ -523,9 +529,11 @@ public String toString() { */ private void startRpcStream() { checkState(adsStream == null, "Previous adsStream has not been cleared yet"); - AggregatedDiscoveryServiceGrpc.AggregatedDiscoveryServiceStub stub = - AggregatedDiscoveryServiceGrpc.newStub(channel); - adsStream = new AdsStream(stub); + if (useProtocolV3) { + adsStream = new AdsStream(); + } else { + adsStream = new AdsStreamV2(); + } adsStream.start(); logger.log(XdsLogLevel.INFO, "ADS stream started"); adsStreamRetryStopwatch.reset().start(); @@ -535,13 +543,9 @@ private void startRpcStream() { * Calls handleLdsResponseForListener or handleLdsResponseForConfigUpdate based on which watcher * was set. */ - private void handleLdsResponse(DiscoveryResponse ldsResponse) { + private void handleLdsResponse(DiscoveryResponseData ldsResponse) { checkState((configWatcher != null) != (listenerWatcher != null), "No LDS request was ever sent. Management server is doing something wrong"); - if (logger.isLoggable(XdsLogLevel.DEBUG)) { - logger.log( - XdsLogLevel.DEBUG, "Received LDS response:\n{0}", respPrinter.print(ldsResponse)); - } if (listenerWatcher != null) { handleLdsResponseForListener(ldsResponse); } else { @@ -557,13 +561,13 @@ private void handleLdsResponse(DiscoveryResponse ldsResponse) { * 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(DiscoveryResponse ldsResponse) { + 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.getResourcesCount()); - List listenerNames = new ArrayList<>(ldsResponse.getResourcesCount()); + 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)) { @@ -576,7 +580,7 @@ private void handleLdsResponseForConfigUpdate(DiscoveryResponse ldsResponse) { } catch (InvalidProtocolBufferException e) { logger.log(XdsLogLevel.WARNING, "Failed to unpack Listeners in LDS response {0}", e); adsStream.sendNackRequest( - ADS_TYPE_URL_LDS_V2, ImmutableList.of(ldsResourceName), + ResourceType.LDS, ImmutableList.of(ldsResourceName), ldsResponse.getVersionInfo(), "Malformed LDS response: " + e); return; } @@ -601,7 +605,7 @@ private void handleLdsResponseForConfigUpdate(DiscoveryResponse ldsResponse) { XdsLogLevel.WARNING, "Failed to unpack HttpConnectionManagers in Listeners of LDS response {0}", e); adsStream.sendNackRequest( - ADS_TYPE_URL_LDS_V2, ImmutableList.of(ldsResourceName), + ResourceType.LDS, ImmutableList.of(ldsResourceName), ldsResponse.getVersionInfo(), "Malformed LDS response: " + e); return; } @@ -647,11 +651,11 @@ private void handleLdsResponseForConfigUpdate(DiscoveryResponse ldsResponse) { if (errorMessage != null) { adsStream.sendNackRequest( - ADS_TYPE_URL_LDS_V2, ImmutableList.of(ldsResourceName), + ResourceType.LDS, ImmutableList.of(ldsResourceName), ldsResponse.getVersionInfo(), errorMessage); return; } - adsStream.sendAckRequest(ADS_TYPE_URL_LDS_V2, ImmutableList.of(ldsResourceName), + adsStream.sendAckRequest(ResourceType.LDS, ImmutableList.of(ldsResourceName), ldsResponse.getVersionInfo()); if (routes != null || rdsRouteConfigName != null) { @@ -673,7 +677,7 @@ private void handleLdsResponseForConfigUpdate(DiscoveryResponse ldsResponse) { logger.log( XdsLogLevel.INFO, "Use RDS to dynamically resolve route config, resource name: {0}", rdsRouteConfigName); - adsStream.sendXdsRequest(ADS_TYPE_URL_RDS_V2, ImmutableList.of(rdsRouteConfigName)); + adsStream.sendXdsRequest(ResourceType.RDS, ImmutableList.of(rdsRouteConfigName)); // Cancel the timer for fetching the previous RDS resource. if (rdsRespTimer != null) { rdsRespTimer.cancel(); @@ -692,13 +696,13 @@ private void handleLdsResponseForConfigUpdate(DiscoveryResponse ldsResponse) { } } - private void handleLdsResponseForListener(DiscoveryResponse ldsResponse) { + 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.getResourcesCount()); + 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)) { @@ -714,7 +718,7 @@ private void handleLdsResponseForListener(DiscoveryResponse ldsResponse) { } catch (InvalidProtocolBufferException e) { logger.log(XdsLogLevel.WARNING, "Failed to unpack Listeners in LDS response {0}", e); adsStream.sendNackRequest( - ADS_TYPE_URL_LDS_V2, ImmutableList.of(), + ResourceType.LDS, ImmutableList.of(), ldsResponse.getVersionInfo(), "Malformed LDS response: " + e); return; } @@ -731,7 +735,7 @@ private void handleLdsResponseForListener(DiscoveryResponse ldsResponse) { } catch (InvalidProtocolBufferException e) { logger.log(XdsLogLevel.WARNING, "Failed to unpack Listener in LDS response {0}", e); adsStream.sendNackRequest( - ADS_TYPE_URL_LDS_V2, ImmutableList.of(), + ResourceType.LDS, ImmutableList.of(), ldsResponse.getVersionInfo(), "Malformed LDS response: " + e); return; } @@ -740,7 +744,7 @@ private void handleLdsResponseForListener(DiscoveryResponse ldsResponse) { listenerWatcher.onResourceDoesNotExist(":" + listenerPort); } } - adsStream.sendAckRequest(ADS_TYPE_URL_LDS_V2, ImmutableList.of(), + adsStream.sendAckRequest(ResourceType.LDS, ImmutableList.of(), ldsResponse.getVersionInfo()); if (listenerUpdate != null) { listenerWatcher.onListenerChanged(listenerUpdate); @@ -777,15 +781,12 @@ private boolean hasMatchingFilter(List filterChainsList) { * 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(DiscoveryResponse rdsResponse) { - if (logger.isLoggable(XdsLogLevel.DEBUG)) { - logger.log(XdsLogLevel.DEBUG, "Received RDS response:\n{0}", respPrinter.print(rdsResponse)); - } + 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.getResourcesCount()); + List routeConfigNames = new ArrayList<>(rdsResponse.getResourcesList().size()); RouteConfiguration requestedRouteConfig = null; try { for (com.google.protobuf.Any res : rdsResponse.getResourcesList()) { @@ -802,7 +803,7 @@ private void handleRdsResponse(DiscoveryResponse rdsResponse) { logger.log( XdsLogLevel.WARNING, "Failed to unpack RouteConfiguration in RDS response {0}", e); adsStream.sendNackRequest( - ADS_TYPE_URL_RDS_V2, ImmutableList.of(adsStream.rdsResourceName), + ResourceType.RDS, ImmutableList.of(adsStream.rdsResourceName), rdsResponse.getVersionInfo(), "Malformed RDS response: " + e); return; } @@ -817,7 +818,7 @@ private void handleRdsResponse(DiscoveryResponse rdsResponse) { } catch (InvalidProtoDataException e) { String errorDetail = e.getMessage(); adsStream.sendNackRequest( - ADS_TYPE_URL_RDS_V2, ImmutableList.of(adsStream.rdsResourceName), + ResourceType.RDS, ImmutableList.of(adsStream.rdsResourceName), rdsResponse.getVersionInfo(), "RouteConfiguration " + requestedRouteConfig.getName() + ": cannot find a " + "valid cluster name in any virtual hosts with domains matching: " @@ -827,7 +828,7 @@ private void handleRdsResponse(DiscoveryResponse rdsResponse) { } } - adsStream.sendAckRequest(ADS_TYPE_URL_RDS_V2, ImmutableList.of(adsStream.rdsResourceName), + adsStream.sendAckRequest(ResourceType.RDS, ImmutableList.of(adsStream.rdsResourceName), rdsResponse.getVersionInfo()); // Notify the ConfigWatcher if this RDS response contains the most recently requested @@ -937,15 +938,12 @@ static VirtualHost findVirtualHostForHostName( * 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(DiscoveryResponse cdsResponse) { - if (logger.isLoggable(XdsLogLevel.DEBUG)) { - logger.log(XdsLogLevel.DEBUG, "Received CDS response:\n{0}", respPrinter.print(cdsResponse)); - } + private void handleCdsResponse(DiscoveryResponseData cdsResponse) { adsStream.cdsRespNonce = cdsResponse.getNonce(); // Unpack Cluster messages. - List clusters = new ArrayList<>(cdsResponse.getResourcesCount()); - List clusterNames = new ArrayList<>(cdsResponse.getResourcesCount()); + 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)) { @@ -958,7 +956,7 @@ private void handleCdsResponse(DiscoveryResponse cdsResponse) { } catch (InvalidProtocolBufferException e) { logger.log(XdsLogLevel.WARNING, "Failed to unpack Clusters in CDS response {0}", e); adsStream.sendNackRequest( - ADS_TYPE_URL_CDS_V2, clusterWatchers.keySet(), + ResourceType.CDS, clusterWatchers.keySet(), cdsResponse.getVersionInfo(), "Malformed CDS response: " + e); return; } @@ -1033,13 +1031,13 @@ private void handleCdsResponse(DiscoveryResponse cdsResponse) { } if (errorMessage != null) { adsStream.sendNackRequest( - ADS_TYPE_URL_CDS_V2, + ResourceType.CDS, clusterWatchers.keySet(), cdsResponse.getVersionInfo(), errorMessage); return; } - adsStream.sendAckRequest(ADS_TYPE_URL_CDS_V2, clusterWatchers.keySet(), + adsStream.sendAckRequest(ResourceType.CDS, clusterWatchers.keySet(), cdsResponse.getVersionInfo()); // Update local CDS cache with data in this response. @@ -1115,15 +1113,11 @@ private static UpstreamTlsContext getTlsContextFromCluster(Cluster cluster) * cached locally, in case of new endpoint watchers interested in the same clusters * are added later. */ - private void handleEdsResponse(DiscoveryResponse edsResponse) { - if (logger.isLoggable(XdsLogLevel.DEBUG)) { - logger.log(XdsLogLevel.DEBUG, "Received EDS response:\n{0}", respPrinter.print(edsResponse)); - } - + private void handleEdsResponse(DiscoveryResponseData edsResponse) { // Unpack ClusterLoadAssignment messages. List clusterLoadAssignments = - new ArrayList<>(edsResponse.getResourcesCount()); - List claNames = new ArrayList<>(edsResponse.getResourcesCount()); + 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)) { @@ -1137,7 +1131,7 @@ private void handleEdsResponse(DiscoveryResponse edsResponse) { logger.log( XdsLogLevel.WARNING, "Failed to unpack ClusterLoadAssignments in EDS response {0}", e); adsStream.sendNackRequest( - ADS_TYPE_URL_EDS_V2, endpointWatchers.keySet(), + ResourceType.EDS, endpointWatchers.keySet(), edsResponse.getVersionInfo(), "Malformed EDS response: " + e); return; } @@ -1210,13 +1204,13 @@ private void handleEdsResponse(DiscoveryResponse edsResponse) { } if (errorMessage != null) { adsStream.sendNackRequest( - ADS_TYPE_URL_EDS_V2, + ResourceType.EDS, endpointWatchers.keySet(), edsResponse.getVersionInfo(), errorMessage); return; } - adsStream.sendAckRequest(ADS_TYPE_URL_EDS_V2, endpointWatchers.keySet(), + adsStream.sendAckRequest(ResourceType.EDS, endpointWatchers.keySet(), edsResponse.getVersionInfo()); // Update local EDS cache by inserting updated endpoint information. @@ -1247,7 +1241,7 @@ final class RpcRetryTask implements Runnable { public void run() { startRpcStream(); if (configWatcher != null) { - adsStream.sendXdsRequest(ADS_TYPE_URL_LDS_V2, ImmutableList.of(ldsResourceName)); + adsStream.sendXdsRequest(ResourceType.LDS, ImmutableList.of(ldsResourceName)); ldsRespTimer = syncContext .schedule( @@ -1255,7 +1249,7 @@ public void run() { INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS, timeService); } if (listenerWatcher != null) { - adsStream.sendXdsRequest(ADS_TYPE_URL_LDS_V2, ImmutableList.of()); + adsStream.sendXdsRequest(ResourceType.LDS, ImmutableList.of()); ldsRespTimer = syncContext .schedule( @@ -1263,7 +1257,7 @@ public void run() { INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS, timeService); } if (!clusterWatchers.isEmpty()) { - adsStream.sendXdsRequest(ADS_TYPE_URL_CDS_V2, clusterWatchers.keySet()); + adsStream.sendXdsRequest(ResourceType.CDS, clusterWatchers.keySet()); for (String clusterName : clusterWatchers.keySet()) { ScheduledHandle timeoutHandle = syncContext @@ -1274,7 +1268,7 @@ public void run() { } } if (!endpointWatchers.isEmpty()) { - adsStream.sendXdsRequest(ADS_TYPE_URL_EDS_V2, endpointWatchers.keySet()); + adsStream.sendXdsRequest(ResourceType.EDS, endpointWatchers.keySet()); for (String clusterName : endpointWatchers.keySet()) { ScheduledHandle timeoutHandle = syncContext @@ -1287,10 +1281,159 @@ public void run() { } } - private final class AdsStream implements StreamObserver { - private final AggregatedDiscoveryServiceGrpc.AggregatedDiscoveryServiceStub stub; + private enum ResourceType { + UNKNOWN, LDS, RDS, CDS, EDS; + + 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); + } + } + + 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); + } + } + + 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; + } + } + } - private StreamObserver requestWriter; + 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; @@ -1321,51 +1464,58 @@ private final class AdsStream implements StreamObserver { @Nullable private String rdsResourceName; - private AdsStream(AggregatedDiscoveryServiceGrpc.AggregatedDiscoveryServiceStub stub) { - this.stub = checkNotNull(stub, "stub"); - } - - private void start() { - requestWriter = stub.withWaitForReady().streamAggregatedResources(this); - } - - @Override - public void onNext(final DiscoveryResponse response) { - syncContext.execute(new Runnable() { - @Override - public void run() { - if (closed) { - return; - } - responseReceived = true; - String typeUrl = response.getTypeUrl(); - // 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. - if (typeUrl.equals(ADS_TYPE_URL_LDS_V2) || typeUrl.equals(ADS_TYPE_URL_LDS)) { - ldsRespNonce = response.getNonce(); - handleLdsResponse(response); - } else if (typeUrl.equals(ADS_TYPE_URL_RDS_V2) || typeUrl.equals(ADS_TYPE_URL_LDS)) { - rdsRespNonce = response.getNonce(); - handleRdsResponse(response); - } else if (typeUrl.equals(ADS_TYPE_URL_CDS_V2) || typeUrl.equals(ADS_TYPE_URL_CDS)) { - cdsRespNonce = response.getNonce(); - handleCdsResponse(response); - } else if (typeUrl.equals(ADS_TYPE_URL_EDS_V2) || typeUrl.equals(ADS_TYPE_URL_EDS)) { - edsRespNonce = response.getNonce(); - handleEdsResponse(response); - } else { - logger.log( - XdsLogLevel.WARNING, - "Received an unknown type of DiscoveryResponse\n{0}", response); - } - } - }); - } - - @Override - public void onError(final Throwable t) { + abstract void start(); + + abstract void sendDiscoveryRequest(DiscoveryRequestData request); + + abstract void sendError(Exception error); + + final void onDiscoveryResponse(final DiscoveryResponseData response) { + syncContext.execute( + new Runnable() { + @Override + public void run() { + 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); + } + } + }); + } + + final void onError(final Throwable t) { syncContext.execute(new Runnable() { @Override public void run() { @@ -1374,8 +1524,7 @@ public void run() { }); } - @Override - public void onCompleted() { + final void onCompleted() { syncContext.execute(new Runnable() { @Override public void run() { @@ -1438,7 +1587,7 @@ private void close(Exception error) { } closed = true; cleanUp(); - requestWriter.onError(error); + sendError(error); } private void cleanUp() { @@ -1452,126 +1601,244 @@ private void cleanUp() { * 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(String typeUrl, Collection resourceNames) { - checkState(requestWriter != null, "ADS stream has not been started"); - String version = ""; - String nonce = ""; - if (typeUrl.equals(ADS_TYPE_URL_LDS_V2)) { - version = ldsVersion; - nonce = ldsRespNonce; - logger.log(XdsLogLevel.INFO, "Sending LDS request for resources: {0}", resourceNames); - } else if (typeUrl.equals(ADS_TYPE_URL_RDS_V2)) { - 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); - } else if (typeUrl.equals(ADS_TYPE_URL_CDS_V2)) { - version = cdsVersion; - nonce = cdsRespNonce; - logger.log(XdsLogLevel.INFO, "Sending CDS request for resources: {0}", resourceNames); - } else if (typeUrl.equals(ADS_TYPE_URL_EDS_V2)) { - version = edsVersion; - nonce = edsRespNonce; - logger.log(XdsLogLevel.INFO, "Sending EDS request for resources: {0}", resourceNames); - } - DiscoveryRequest request = - DiscoveryRequest - .newBuilder() - .setVersionInfo(version) - .setNode(node) - .addAllResourceNames(resourceNames) - .setTypeUrl(typeUrl) - .setResponseNonce(nonce) - .build(); - requestWriter.onNext(request); - logger.log(XdsLogLevel.DEBUG, "Sent DiscoveryRequest\n{0}", request); + 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(String typeUrl, Collection resourceNames, + private void sendAckRequest(ResourceType resourceType, Collection resourceNames, String versionInfo) { - checkState(requestWriter != null, "ADS stream has not been started"); - String nonce = ""; - if (typeUrl.equals(ADS_TYPE_URL_LDS_V2)) { - ldsVersion = versionInfo; - nonce = ldsRespNonce; - } else if (typeUrl.equals(ADS_TYPE_URL_RDS_V2)) { - rdsVersion = versionInfo; - nonce = rdsRespNonce; - } else if (typeUrl.equals(ADS_TYPE_URL_CDS_V2)) { - cdsVersion = versionInfo; - nonce = cdsRespNonce; - } else if (typeUrl.equals(ADS_TYPE_URL_EDS_V2)) { - edsVersion = versionInfo; - nonce = edsRespNonce; - } - DiscoveryRequest request = - DiscoveryRequest - .newBuilder() - .setVersionInfo(versionInfo) - .setNode(node) - .addAllResourceNames(resourceNames) - .setTypeUrl(typeUrl) - .setResponseNonce(nonce) - .build(); - requestWriter.onNext(request); - logger.log(XdsLogLevel.DEBUG, "Sent ACK request\n{0}", request); + 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(String typeUrl, Collection resourceNames, + 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(channel); + } + + @Override + void start() { + StreamObserver responseReaderV2 = + new StreamObserver() { + @Override + public void onNext(io.envoyproxy.envoy.api.v2.DiscoveryResponse response) { + DiscoveryResponseData responseData = + DiscoveryResponseData.fromEnvoyProtoV2(response); + if (logger.isLoggable(XdsLogLevel.DEBUG)) { + logger.log( + XdsLogLevel.DEBUG, + "Received {0} response:\n{1}", + responseData.getResourceType(), + respPrinter.print(response)); + } + onDiscoveryResponse(responseData); + } + + @Override + public void onError(Throwable t) { + AdsStreamV2.this.onError(t); + } + + @Override + public void onCompleted() { + AdsStreamV2.this.onCompleted(); + } + }; + 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(channel); + } + + @Override + void start() { + StreamObserver responseReader = new StreamObserver() { + @Override + public void onNext(DiscoveryResponse response) { + DiscoveryResponseData responseData = + DiscoveryResponseData.fromEnvoyProto(response); + if (logger.isLoggable(XdsLogLevel.DEBUG)) { + logger.log( + XdsLogLevel.DEBUG, + "Received {0} response:\n{1}", + responseData.getResourceType(), + respPrinter.print(response)); + } + onDiscoveryResponse(responseData); + } + + @Override + public void onError(Throwable t) { + AdsStream.this.onError(t); + } + + @Override + public void onCompleted() { + AdsStream.this.onCompleted(); + } + }; + requestWriter = stub.withWaitForReady().streamAggregatedResources(responseReader); + } + + @Override + void sendDiscoveryRequest(DiscoveryRequestData request) { checkState(requestWriter != null, "ADS stream has not been started"); - String versionInfo = ""; - String nonce = ""; - if (typeUrl.equals(ADS_TYPE_URL_LDS_V2)) { - versionInfo = ldsVersion; - nonce = ldsRespNonce; - logger.log( - XdsLogLevel.WARNING, - "Rejecting LDS update, version: {0}, reason: {1}", rejectVersion, message); - } else if (typeUrl.equals(ADS_TYPE_URL_RDS_V2)) { - versionInfo = rdsVersion; - nonce = rdsRespNonce; - logger.log( - XdsLogLevel.WARNING, - "Rejecting RDS update, version: {0}, reason: {1}", rejectVersion, message); - } else if (typeUrl.equals(ADS_TYPE_URL_CDS_V2)) { - versionInfo = cdsVersion; - nonce = cdsRespNonce; - logger.log( - XdsLogLevel.WARNING, - "Rejecting CDS update, version: {0}, reason: {1}", rejectVersion, message); - } else if (typeUrl.equals(ADS_TYPE_URL_EDS_V2)) { - versionInfo = edsVersion; - nonce = edsRespNonce; - logger.log( - XdsLogLevel.WARNING, - "Rejecting EDS update, version: {0}, reason: {1}", rejectVersion, message); - } - DiscoveryRequest request = - DiscoveryRequest - .newBuilder() - .setVersionInfo(versionInfo) - .setNode(node) - .addAllResourceNames(resourceNames) - .setTypeUrl(typeUrl) - .setResponseNonce(nonce) - .setErrorDetail( - com.google.rpc.Status.newBuilder() - .setCode(Code.INVALID_ARGUMENT_VALUE) - .setMessage(message)) - .build(); - requestWriter.onNext(request); - logger.log(XdsLogLevel.DEBUG, "Sent NACK request\n{0}", request); + DiscoveryRequest requestProto = request.toEnvoyProto(); + requestWriter.onNext(requestProto); + logger.log(XdsLogLevel.DEBUG, "Sent DiscoveryRequest\n{0}", requestProto); + } + + @Override + void sendError(Exception error) { + requestWriter.onError(error); } } diff --git a/xds/src/main/java/io/grpc/xds/XdsClientWrapperForServerSds.java b/xds/src/main/java/io/grpc/xds/XdsClientWrapperForServerSds.java index 79873e9dfa0..95e7cb9e06f 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClientWrapperForServerSds.java +++ b/xds/src/main/java/io/grpc/xds/XdsClientWrapperForServerSds.java @@ -20,7 +20,6 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.annotations.VisibleForTesting; -import io.envoyproxy.envoy.api.v2.core.Node; import io.grpc.Internal; import io.grpc.InternalLogId; import io.grpc.Status; @@ -28,6 +27,7 @@ import io.grpc.internal.ExponentialBackoffPolicy; import io.grpc.internal.GrpcUtil; import io.grpc.internal.SharedResourceHolder; +import io.grpc.xds.EnvoyProtoData.Node; import io.grpc.xds.EnvoyServerProtoData.CidrRange; import io.grpc.xds.EnvoyServerProtoData.DownstreamTlsContext; import io.grpc.xds.EnvoyServerProtoData.FilterChain; diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 47946949b49..e0c03e5f907 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -26,7 +26,6 @@ import com.google.common.collect.Iterables; import com.google.gson.Gson; import com.google.re2j.Pattern; -import io.envoyproxy.envoy.api.v2.core.Node; import io.grpc.Attributes; import io.grpc.EquivalentAddressGroup; import io.grpc.InternalLogId; @@ -39,6 +38,7 @@ import io.grpc.xds.Bootstrapper.BootstrapInfo; import io.grpc.xds.Bootstrapper.ServerInfo; import io.grpc.xds.EnvoyProtoData.ClusterWeight; +import io.grpc.xds.EnvoyProtoData.Node; import io.grpc.xds.EnvoyProtoData.Route; import io.grpc.xds.EnvoyProtoData.RouteAction; import io.grpc.xds.RouteMatch.FractionMatcher; diff --git a/xds/src/main/java/io/grpc/xds/internal/sds/ClientSslContextProviderFactory.java b/xds/src/main/java/io/grpc/xds/internal/sds/ClientSslContextProviderFactory.java index bb6a636e969..84b2f8284aa 100644 --- a/xds/src/main/java/io/grpc/xds/internal/sds/ClientSslContextProviderFactory.java +++ b/xds/src/main/java/io/grpc/xds/internal/sds/ClientSslContextProviderFactory.java @@ -43,7 +43,7 @@ public SslContextProvider create(UpstreamTlsContext upstreamTlsContext) { try { return SdsClientSslContextProvider.getProvider( upstreamTlsContext, - Bootstrapper.getInstance().readBootstrap().getNode(), + Bootstrapper.getInstance().readBootstrap().getNode().toEnvoyProtoNodeV2(), Executors.newSingleThreadExecutor(new ThreadFactoryBuilder() .setNameFormat("client-sds-sslcontext-provider-%d") .setDaemon(true) diff --git a/xds/src/main/java/io/grpc/xds/internal/sds/ServerSslContextProviderFactory.java b/xds/src/main/java/io/grpc/xds/internal/sds/ServerSslContextProviderFactory.java index fb271a32dc7..1cd2cfa8e9b 100644 --- a/xds/src/main/java/io/grpc/xds/internal/sds/ServerSslContextProviderFactory.java +++ b/xds/src/main/java/io/grpc/xds/internal/sds/ServerSslContextProviderFactory.java @@ -45,7 +45,7 @@ public SslContextProvider create( try { return SdsServerSslContextProvider.getProvider( downstreamTlsContext, - Bootstrapper.getInstance().readBootstrap().getNode(), + Bootstrapper.getInstance().readBootstrap().getNode().toEnvoyProtoNodeV2(), Executors.newSingleThreadExecutor(new ThreadFactoryBuilder() .setNameFormat("server-sds-sslcontext-provider-%d") .setDaemon(true) diff --git a/xds/src/test/java/io/grpc/xds/BootstrapperTest.java b/xds/src/test/java/io/grpc/xds/BootstrapperTest.java index 37ef4161259..6c1fd2111ba 100644 --- a/xds/src/test/java/io/grpc/xds/BootstrapperTest.java +++ b/xds/src/test/java/io/grpc/xds/BootstrapperTest.java @@ -18,15 +18,14 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; -import com.google.protobuf.Struct; -import com.google.protobuf.Value; -import io.envoyproxy.envoy.api.v2.core.Locality; -import io.envoyproxy.envoy.api.v2.core.Node; import io.grpc.internal.GrpcUtil; import io.grpc.internal.GrpcUtil.GrpcBuildVersion; import io.grpc.xds.Bootstrapper.BootstrapInfo; import io.grpc.xds.Bootstrapper.ServerInfo; +import io.grpc.xds.EnvoyProtoData.Locality; +import io.grpc.xds.EnvoyProtoData.Node; import java.io.IOException; import java.util.List; import org.junit.Rule; @@ -82,16 +81,13 @@ public void parseBootstrap_validData_singleXdsServer() throws IOException { getNodeBuilder() .setId("ENVOY_NODE_ID") .setCluster("ENVOY_CLUSTER") - .setLocality( - Locality.newBuilder() - .setRegion("ENVOY_REGION").setZone("ENVOY_ZONE").setSubZone("ENVOY_SUBZONE")) + .setLocality(new Locality("ENVOY_REGION", "ENVOY_ZONE", "ENVOY_SUBZONE")) .setMetadata( - Struct.newBuilder() - .putFields("TRAFFICDIRECTOR_INTERCEPTION_PORT", - Value.newBuilder().setStringValue("ENVOY_PORT").build()) - .putFields("TRAFFICDIRECTOR_NETWORK_NAME", - Value.newBuilder().setStringValue("VPC_NETWORK_NAME").build()) - .build()) + ImmutableMap.of( + "TRAFFICDIRECTOR_INTERCEPTION_PORT", + "ENVOY_PORT", + "TRAFFICDIRECTOR_NETWORK_NAME", + "VPC_NETWORK_NAME")) .build()); } @@ -116,6 +112,9 @@ public void parseBootstrap_validData_multipleXdsServers() throws IOException { + " \"server_uri\": \"trafficdirector-foo.googleapis.com:443\",\n" + " \"channel_creds\": [\n" + " {\"type\": \"tls\"}, {\"type\": \"loas\"}, {\"type\": \"google_default\"}\n" + + " ],\n" + + " \"server_features\": [\n" + + " \"xds_v3\", \"foo\", \"bar\"\n" + " ]\n" + " },\n" + " {\n" @@ -138,6 +137,7 @@ public void parseBootstrap_validData_multipleXdsServers() throws IOException { assertThat(serverInfoList.get(0).getChannelCredentials().get(2).getType()) .isEqualTo("google_default"); assertThat(serverInfoList.get(0).getChannelCredentials().get(2).getConfig()).isNull(); + assertThat(serverInfoList.get(0).getServerFeatures()).contains("xds_v3"); assertThat(serverInfoList.get(1).getServerUri()) .isEqualTo("trafficdirector-bar.googleapis.com:443"); assertThat(serverInfoList.get(1).getChannelCredentials()).isEmpty(); @@ -145,16 +145,13 @@ public void parseBootstrap_validData_multipleXdsServers() throws IOException { getNodeBuilder() .setId("ENVOY_NODE_ID") .setCluster("ENVOY_CLUSTER") - .setLocality( - Locality.newBuilder() - .setRegion("ENVOY_REGION").setZone("ENVOY_ZONE").setSubZone("ENVOY_SUBZONE")) + .setLocality(new Locality("ENVOY_REGION", "ENVOY_ZONE", "ENVOY_SUBZONE")) .setMetadata( - Struct.newBuilder() - .putFields("TRAFFICDIRECTOR_INTERCEPTION_PORT", - Value.newBuilder().setStringValue("ENVOY_PORT").build()) - .putFields("TRAFFICDIRECTOR_NETWORK_NAME", - Value.newBuilder().setStringValue("VPC_NETWORK_NAME").build()) - .build()) + ImmutableMap.of( + "TRAFFICDIRECTOR_INTERCEPTION_PORT", + "ENVOY_PORT", + "TRAFFICDIRECTOR_NETWORK_NAME", + "VPC_NETWORK_NAME")) .build()); } @@ -201,16 +198,13 @@ public void parseBootstrap_IgnoreIrrelevantFields() throws IOException { getNodeBuilder() .setId("ENVOY_NODE_ID") .setCluster("ENVOY_CLUSTER") - .setLocality( - Locality.newBuilder() - .setRegion("ENVOY_REGION").setZone("ENVOY_ZONE").setSubZone("ENVOY_SUBZONE")) + .setLocality(new Locality("ENVOY_REGION", "ENVOY_ZONE", "ENVOY_SUBZONE")) .setMetadata( - Struct.newBuilder() - .putFields("TRAFFICDIRECTOR_INTERCEPTION_PORT", - Value.newBuilder().setStringValue("ENVOY_PORT").build()) - .putFields("TRAFFICDIRECTOR_NETWORK_NAME", - Value.newBuilder().setStringValue("VPC_NETWORK_NAME").build()) - .build()) + ImmutableMap.of( + "TRAFFICDIRECTOR_INTERCEPTION_PORT", + "ENVOY_PORT", + "TRAFFICDIRECTOR_NETWORK_NAME", + "VPC_NETWORK_NAME")) .build()); } @@ -304,7 +298,6 @@ public void parseBootstrap_serverWithoutServerUri() throws IOException { Bootstrapper.parseConfig(rawData); } - @SuppressWarnings("deprecation") private static Node.Builder getNodeBuilder() { GrpcBuildVersion buildVersion = GrpcUtil.getGrpcBuildVersion(); return diff --git a/xds/src/test/java/io/grpc/xds/CdsLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/CdsLoadBalancerTest.java index 34d57be999b..9a9bb69241f 100644 --- a/xds/src/test/java/io/grpc/xds/CdsLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/CdsLoadBalancerTest.java @@ -29,7 +29,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -61,8 +60,6 @@ import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.XdsClient.ClusterUpdate; import io.grpc.xds.XdsClient.ClusterWatcher; -import io.grpc.xds.XdsClient.EndpointUpdate; -import io.grpc.xds.XdsClient.EndpointWatcher; import io.grpc.xds.XdsClient.RefCountedXdsClientObjectPool; import io.grpc.xds.XdsClient.XdsClientFactory; import io.grpc.xds.internal.sds.CommonTlsContextTestsUtil; @@ -592,52 +589,4 @@ public void clusterWatcher_onErrorCalledBeforeAndAfterOnClusterChanged() { verify(helper, times(1)) .updateBalancingState(eq(TRANSIENT_FAILURE), any(SubchannelPicker.class)); } - - @Test - public void cdsBalancerIntegrateWithEdsBalancer() { - lbRegistry.deregister(fakeEdsLoadBlancerProvider); - lbRegistry.register(new EdsLoadBalancerProvider()); - - ResolvedAddresses resolvedAddresses1 = ResolvedAddresses.newBuilder() - .setAddresses(ImmutableList.of()) - .setAttributes(Attributes.newBuilder() - .set(XdsAttributes.XDS_CLIENT_POOL, xdsClientPool) - .build()) - .setLoadBalancingPolicyConfig(new CdsConfig("foo.googleapis.com")) - .build(); - cdsLoadBalancer.handleResolvedAddresses(resolvedAddresses1); - ArgumentCaptor clusterWatcherCaptor = ArgumentCaptor.forClass(null); - verify(xdsClient).watchClusterData(eq("foo.googleapis.com"), clusterWatcherCaptor.capture()); - ClusterWatcher clusterWatcher = clusterWatcherCaptor.getValue(); - clusterWatcher.onClusterChanged( - ClusterUpdate.newBuilder() - .setClusterName("foo.googleapis.com") - .setEdsServiceName("edsServiceFoo.googleapis.com") - .setLbPolicy("round_robin") - .build()); - - ArgumentCaptor endpointWatcherCaptor = ArgumentCaptor.forClass(null); - verify(xdsClient).watchEndpointData( - eq("edsServiceFoo.googleapis.com"), endpointWatcherCaptor.capture()); - EndpointWatcher endpointWatcher = endpointWatcherCaptor.getValue(); - - verify(helper, never()).updateBalancingState( - eq(TRANSIENT_FAILURE), any(SubchannelPicker.class)); - // Update endpoints with all backends unhealthy, the EDS will update channel state to - // TRANSIENT_FAILURE. - // Not able to test with healthy endpoints because the real EDS balancer is using real - // round-robin balancer to balance endpoints. - endpointWatcher.onEndpointChanged(EndpointUpdate.newBuilder() - .setClusterName("edsServiceFoo.googleapis.com") - .addLocalityLbEndpoints( - new EnvoyProtoData.Locality("region", "zone", "subzone"), - new EnvoyProtoData.LocalityLbEndpoints( - // All unhealthy. - ImmutableList.of(new EnvoyProtoData.LbEndpoint("127.0.0.1", 8080, 1, false)), 1, 0)) - .build()); - verify(helper, atLeastOnce()).updateBalancingState( - eq(TRANSIENT_FAILURE), any(SubchannelPicker.class)); - - cdsLoadBalancer.shutdown(); - } } diff --git a/xds/src/test/java/io/grpc/xds/ClientLoadCounterTest.java b/xds/src/test/java/io/grpc/xds/ClientLoadCounterTest.java index bedeaf4eaea..db3fbb818d2 100644 --- a/xds/src/test/java/io/grpc/xds/ClientLoadCounterTest.java +++ b/xds/src/test/java/io/grpc/xds/ClientLoadCounterTest.java @@ -77,9 +77,11 @@ public void snapshotContainsDataInCounter() { long numInProgressCalls = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); long numFailedCalls = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); long numIssuedCalls = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); - counter = - new ClientLoadCounter(numSucceededCalls, numInProgressCalls, numFailedCalls, - numIssuedCalls); + counter = new ClientLoadCounter(); + counter.setCallsSucceeded(numSucceededCalls); + counter.setCallsInProgress(numInProgressCalls); + counter.setCallsFailed(numFailedCalls); + counter.setCallsIssued(numIssuedCalls); ClientLoadSnapshot snapshot = counter.snapshot(); assertQueryCounts(snapshot, numSucceededCalls, numInProgressCalls, numFailedCalls, numIssuedCalls); diff --git a/xds/src/test/java/io/grpc/xds/EdsLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/EdsLoadBalancerTest.java index ff9101b5a60..2edfd673f72 100644 --- a/xds/src/test/java/io/grpc/xds/EdsLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/EdsLoadBalancerTest.java @@ -22,7 +22,7 @@ import static io.grpc.ConnectivityState.READY; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; import static io.grpc.xds.XdsClientTestHelper.buildClusterLoadAssignment; -import static io.grpc.xds.XdsClientTestHelper.buildDiscoveryResponse; +import static io.grpc.xds.XdsClientTestHelper.buildDiscoveryResponseV2; import static io.grpc.xds.XdsClientTestHelper.buildDropOverload; import static io.grpc.xds.XdsClientTestHelper.buildLbEndpoint; import static io.grpc.xds.XdsClientTestHelper.buildLocalityLbEndpoints; @@ -43,7 +43,6 @@ import io.envoyproxy.envoy.api.v2.ClusterLoadAssignment.Policy.DropOverload; import io.envoyproxy.envoy.api.v2.DiscoveryRequest; import io.envoyproxy.envoy.api.v2.DiscoveryResponse; -import io.envoyproxy.envoy.api.v2.core.Node; import io.envoyproxy.envoy.api.v2.endpoint.LbEndpoint; import io.envoyproxy.envoy.api.v2.endpoint.LocalityLbEndpoints; import io.envoyproxy.envoy.service.discovery.v2.AggregatedDiscoveryServiceGrpc.AggregatedDiscoveryServiceImplBase; @@ -76,8 +75,11 @@ import io.grpc.xds.Bootstrapper.ChannelCreds; import io.grpc.xds.Bootstrapper.ServerInfo; import io.grpc.xds.EdsLoadBalancerProvider.EdsConfig; +import io.grpc.xds.EnvoyProtoData.Node; +import io.grpc.xds.LoadStatsManager.LoadStatsStore; import io.grpc.xds.LocalityStore.LocalityStoreFactory; import io.grpc.xds.XdsClient.EndpointUpdate; +import io.grpc.xds.XdsClient.XdsChannel; import io.grpc.xds.XdsClient.XdsChannelFactory; import java.net.InetSocketAddress; import java.util.ArrayDeque; @@ -135,10 +137,10 @@ public void uncaughtException(Thread t, Throwable e) { private final Map childBalancers = new HashMap<>(); private final XdsChannelFactory channelFactory = new XdsChannelFactory() { @Override - ManagedChannel createChannel(List servers) { + XdsChannel createChannel(List servers) { assertThat(Iterables.getOnlyElement(servers).getServerUri()) .isEqualTo("trafficdirector.googleapis.com"); - return channel; + return new XdsChannel(channel, false); } }; @@ -228,10 +230,10 @@ public StreamObserver streamAggregatedResources( .forName(serverName) .directExecutor() .build()); - final List serverList = - ImmutableList.of( - new ServerInfo("trafficdirector.googleapis.com", ImmutableList.of())); - BootstrapInfo bootstrapInfo = new BootstrapInfo(serverList, Node.getDefaultInstance()); + final List serverList = ImmutableList.of( + new ServerInfo("trafficdirector.googleapis.com", ImmutableList.of(), null)); + Node node = Node.newBuilder().build(); + BootstrapInfo bootstrapInfo = new BootstrapInfo(serverList, node); doReturn(bootstrapInfo).when(bootstrapper).readBootstrap(); if (isFullFlow) { @@ -240,7 +242,7 @@ public StreamObserver streamAggregatedResources( SERVICE_AUTHORITY, serverList, channelFactory, - Node.getDefaultInstance(), + node, syncContext, fakeClock.getScheduledExecutorService(), mock(BackoffPolicy.Provider.class), @@ -679,7 +681,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { // The whole cluster is no longer accessible. // Note that EDS resource removal is achieved by CDS resource update. responseObserver.onNext( - buildDiscoveryResponse( + buildDiscoveryResponseV2( String.valueOf(versionIno++), Collections.emptyList(), XdsClientImpl.ADS_TYPE_URL_CDS_V2, @@ -767,7 +769,7 @@ private void deliverResolvedAddresses( private void deliverClusterLoadAssignments(ClusterLoadAssignment clusterLoadAssignment) { responseObserver.onNext( - buildDiscoveryResponse( + buildDiscoveryResponseV2( String.valueOf(versionIno++), ImmutableList.of(Any.pack(clusterLoadAssignment)), XdsClientImpl.ADS_TYPE_URL_EDS_V2, diff --git a/xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java b/xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java index 7e633d55bd5..24e739bf560 100644 --- a/xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java +++ b/xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java @@ -18,9 +18,13 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.common.collect.ImmutableMap; import com.google.common.testing.EqualsTester; import com.google.protobuf.BoolValue; +import com.google.protobuf.Struct; import com.google.protobuf.UInt32Value; +import com.google.protobuf.Value; +import com.google.protobuf.util.Durations; import com.google.re2j.Pattern; import io.envoyproxy.envoy.config.core.v3.RuntimeFractionalPercent; import io.envoyproxy.envoy.config.route.v3.QueryParameterMatcher; @@ -29,8 +33,10 @@ import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher; import io.envoyproxy.envoy.type.v3.FractionalPercent; import io.envoyproxy.envoy.type.v3.Int64Range; +import io.grpc.xds.EnvoyProtoData.Address; import io.grpc.xds.EnvoyProtoData.ClusterWeight; import io.grpc.xds.EnvoyProtoData.Locality; +import io.grpc.xds.EnvoyProtoData.Node; import io.grpc.xds.EnvoyProtoData.Route; import io.grpc.xds.EnvoyProtoData.RouteAction; import io.grpc.xds.EnvoyProtoData.StructOrError; @@ -39,6 +45,7 @@ import io.grpc.xds.RouteMatch.PathMatcher; import java.util.Arrays; import java.util.Collections; +import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import org.junit.Test; import org.junit.runner.RunWith; @@ -63,7 +70,8 @@ public void locality_convertToAndFromLocalityProto() { assertThat(xdsLocality.getZone()).isEqualTo("test_zone"); assertThat(xdsLocality.getSubZone()).isEqualTo("test_subzone"); - io.envoyproxy.envoy.api.v2.core.Locality convertedLocality = xdsLocality.toEnvoyProtoLocality(); + io.envoyproxy.envoy.api.v2.core.Locality convertedLocality = + xdsLocality.toEnvoyProtoLocalityV2(); assertThat(convertedLocality.getRegion()).isEqualTo("test_region"); assertThat(convertedLocality.getZone()).isEqualTo("test_zone"); assertThat(convertedLocality.getSubZone()).isEqualTo("test_subzone"); @@ -84,6 +92,95 @@ public void locality_equal() { .testEquals(); } + @Test + public void convertNode() { + Node node = Node.newBuilder() + .setId("node-id") + .setCluster("cluster") + .setMetadata( + ImmutableMap.of( + "TRAFFICDIRECTOR_INTERCEPTION_PORT", + "ENVOY_PORT", + "TRAFFICDIRECTOR_NETWORK_NAME", + "VPC_NETWORK_NAME")) + .setLocality(new Locality("region", "zone", "subzone")) + .addListeningAddresses(new Address("www.foo.com", 8080)) + .addListeningAddresses(new Address("www.bar.com", 8088)) + .setBuildVersion("v1") + .setUserAgentName("agent") + .setUserAgentVersion("1.1") + .addClientFeatures("feature-1") + .addClientFeatures("feature-2") + .build(); + io.envoyproxy.envoy.config.core.v3.Node nodeProto = + io.envoyproxy.envoy.config.core.v3.Node.newBuilder() + .setId("node-id") + .setCluster("cluster") + .setMetadata(Struct.newBuilder() + .putFields("TRAFFICDIRECTOR_INTERCEPTION_PORT", + Value.newBuilder().setStringValue("ENVOY_PORT").build()) + .putFields("TRAFFICDIRECTOR_NETWORK_NAME", + Value.newBuilder().setStringValue("VPC_NETWORK_NAME").build())) + .setLocality( + io.envoyproxy.envoy.config.core.v3.Locality.newBuilder() + .setRegion("region") + .setZone("zone") + .setSubZone("subzone")) + .addListeningAddresses( + io.envoyproxy.envoy.config.core.v3.Address.newBuilder() + .setSocketAddress( + io.envoyproxy.envoy.config.core.v3.SocketAddress.newBuilder() + .setAddress("www.foo.com") + .setPortValue(8080))) + .addListeningAddresses( + io.envoyproxy.envoy.config.core.v3.Address.newBuilder() + .setSocketAddress( + io.envoyproxy.envoy.config.core.v3.SocketAddress.newBuilder() + .setAddress("www.bar.com") + .setPortValue(8088))) + .setUserAgentName("agent") + .setUserAgentVersion("1.1") + .addClientFeatures("feature-1") + .addClientFeatures("feature-2") + .build(); + assertThat(node.toEnvoyProtoNode()).isEqualTo(nodeProto); + + @SuppressWarnings("deprecation") // Deprecated v2 API setBuildVersion(). + io.envoyproxy.envoy.api.v2.core.Node nodeProtoV2 = + io.envoyproxy.envoy.api.v2.core.Node.newBuilder() + .setId("node-id") + .setCluster("cluster") + .setMetadata(Struct.newBuilder() + .putFields("TRAFFICDIRECTOR_INTERCEPTION_PORT", + Value.newBuilder().setStringValue("ENVOY_PORT").build()) + .putFields("TRAFFICDIRECTOR_NETWORK_NAME", + Value.newBuilder().setStringValue("VPC_NETWORK_NAME").build())) + .setLocality( + io.envoyproxy.envoy.api.v2.core.Locality.newBuilder() + .setRegion("region") + .setZone("zone") + .setSubZone("subzone")) + .addListeningAddresses( + io.envoyproxy.envoy.api.v2.core.Address.newBuilder() + .setSocketAddress( + io.envoyproxy.envoy.api.v2.core.SocketAddress.newBuilder() + .setAddress("www.foo.com") + .setPortValue(8080))) + .addListeningAddresses( + io.envoyproxy.envoy.api.v2.core.Address.newBuilder() + .setSocketAddress( + io.envoyproxy.envoy.api.v2.core.SocketAddress.newBuilder() + .setAddress("www.bar.com") + .setPortValue(8088))) + .setBuildVersion("v1") + .setUserAgentName("agent") + .setUserAgentVersion("1.1") + .addClientFeatures("feature-1") + .addClientFeatures("feature-2") + .build(); + assertThat(node.toEnvoyProtoNodeV2()).isEqualTo(nodeProtoV2); + } + @Test public void locality_hash() { assertThat(new Locality("region", "zone", "subzone").hashCode()) @@ -111,7 +208,7 @@ public void convertRoute() { new Route( new RouteMatch(new PathMatcher("/service/method", null, null), Collections.emptyList(), null), - new RouteAction("cluster-foo", null))); + new RouteAction(TimeUnit.SECONDS.toNanos(15L), "cluster-foo", null))); io.envoyproxy.envoy.config.route.v3.Route unsupportedProto = io.envoyproxy.envoy.config.route.v3.Route.newBuilder() @@ -298,27 +395,51 @@ public void convertRouteMatch_withRuntimeFraction() { @Test public void convertRouteAction() { - // cluster_specifier = cluster + // cluster_specifier = cluster, default timeout io.envoyproxy.envoy.config.route.v3.RouteAction proto1 = io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() .setCluster("cluster-foo") .build(); StructOrError struct1 = RouteAction.fromEnvoyProtoRouteAction(proto1); assertThat(struct1.getErrorDetail()).isNull(); + assertThat(struct1.getStruct().getTimeoutNano()) + .isEqualTo(TimeUnit.SECONDS.toNanos(15L)); // default value assertThat(struct1.getStruct().getCluster()).isEqualTo("cluster-foo"); assertThat(struct1.getStruct().getWeightedCluster()).isNull(); - // cluster_specifier = cluster_header + // cluster_specifier = cluster, infinity timeout io.envoyproxy.envoy.config.route.v3.RouteAction proto2 = io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() - .setClusterHeader("cluster-bar") + .setMaxGrpcTimeout(Durations.fromNanos(0)) + .setTimeout(Durations.fromMicros(20L)) + .setCluster("cluster-foo") .build(); StructOrError struct2 = RouteAction.fromEnvoyProtoRouteAction(proto2); - assertThat(struct2).isNull(); + assertThat(struct2.getStruct().getTimeoutNano()) + .isEqualTo(Long.MAX_VALUE); // infinite - // cluster_specifier = weighted_cluster + // cluster_specifier = cluster, infinity timeout io.envoyproxy.envoy.config.route.v3.RouteAction proto3 = io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setTimeout(Durations.fromNanos(0)) + .setCluster("cluster-foo") + .build(); + StructOrError struct3 = RouteAction.fromEnvoyProtoRouteAction(proto3); + assertThat(struct3.getStruct().getTimeoutNano()).isEqualTo(Long.MAX_VALUE); // infinite + + // cluster_specifier = cluster_header + io.envoyproxy.envoy.config.route.v3.RouteAction proto4 = + io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setClusterHeader("cluster-bar") + .build(); + StructOrError struct4 = RouteAction.fromEnvoyProtoRouteAction(proto4); + assertThat(struct4).isNull(); + + // cluster_specifier = weighted_cluster + io.envoyproxy.envoy.config.route.v3.RouteAction proto5 = + io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder() + .setMaxGrpcTimeout(Durations.fromSeconds(6L)) + .setTimeout(Durations.fromMicros(20L)) .setWeightedClusters( WeightedCluster.newBuilder() .addClusters( @@ -327,10 +448,12 @@ public void convertRouteAction() { .setName("cluster-baz") .setWeight(UInt32Value.newBuilder().setValue(100)))) .build(); - StructOrError struct3 = RouteAction.fromEnvoyProtoRouteAction(proto3); - assertThat(struct3.getErrorDetail()).isNull(); - assertThat(struct3.getStruct().getCluster()).isNull(); - assertThat(struct3.getStruct().getWeightedCluster()) + StructOrError struct5 = RouteAction.fromEnvoyProtoRouteAction(proto5); + assertThat(struct5.getErrorDetail()).isNull(); + assertThat(struct5.getStruct().getTimeoutNano()) + .isEqualTo(TimeUnit.SECONDS.toNanos(6L)); + assertThat(struct5.getStruct().getCluster()).isNull(); + assertThat(struct5.getStruct().getWeightedCluster()) .containsExactly(new ClusterWeight("cluster-baz", 100)); // cluster_specifier unset diff --git a/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java b/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java index 8c2e6f5a9fc..be68f08b110 100644 --- a/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java +++ b/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.util.concurrent.MoreExecutors; @@ -44,7 +45,6 @@ import io.envoyproxy.envoy.service.load_stats.v2.LoadStatsResponse; import io.grpc.Context; import io.grpc.Context.CancellationListener; -import io.grpc.InternalLogId; import io.grpc.ManagedChannel; import io.grpc.Status; import io.grpc.SynchronizationContext; @@ -54,16 +54,19 @@ import io.grpc.internal.FakeClock; import io.grpc.stub.StreamObserver; import io.grpc.testing.GrpcCleanupRule; -import io.grpc.xds.LoadReportClient.LoadReportCallback; +import io.grpc.xds.LoadStatsManager.LoadStatsStore; +import io.grpc.xds.LoadStatsManager.LoadStatsStoreFactory; import java.util.ArrayDeque; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import javax.annotation.Nullable; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -93,6 +96,8 @@ public class LoadReportClientTest { "TRAFFICDIRECTOR_NETWORK_HOSTNAME", Value.newBuilder().setStringValue("default").build())) .build(); + private static final String CLUSTER1 = "cluster-foo.googleapis.com"; + private static final String CLUSTER2 = "cluster-bar.googleapis.com"; private static final FakeClock.TaskFilter LOAD_REPORTING_TASK_FILTER = new FakeClock.TaskFilter() { @Override @@ -119,11 +124,18 @@ public void uncaughtException(Thread t, Throwable e) { throw new AssertionError(e); } }); - private final InternalLogId logId = InternalLogId.allocate("lrs-client-test", null); private final FakeClock fakeClock = new FakeClock(); private final ArrayDeque> lrsRequestObservers = new ArrayDeque<>(); private final AtomicBoolean callEnded = new AtomicBoolean(true); + private final LoadStatsManager loadStatsManager = + new LoadStatsManager(new LoadStatsStoreFactory() { + @Override + public LoadStatsStore newLoadStatsStore(String cluster, String clusterService) { + return new FakeLoadStatsStore( + cluster, clusterService, fakeClock.getStopwatchSupplier().get()); + } + }); @Mock private BackoffPolicy.Provider backoffPolicyProvider; @@ -131,12 +143,6 @@ public void uncaughtException(Thread t, Throwable e) { private BackoffPolicy backoffPolicy1; @Mock private BackoffPolicy backoffPolicy2; - @Mock - private LoadStatsStore loadStatsStore1; - @Mock - private LoadStatsStore loadStatsStore2; - @Mock - private LoadReportCallback callback; @Captor private ArgumentCaptor> lrsResponseObserverCaptor; @@ -178,18 +184,18 @@ public void cancelled(Context context) { when(backoffPolicy1.nextBackoffNanos()) .thenReturn(TimeUnit.SECONDS.toNanos(1L), TimeUnit.SECONDS.toNanos(10L)); when(backoffPolicy2.nextBackoffNanos()) - .thenReturn(TimeUnit.SECONDS.toNanos(1L), TimeUnit.SECONDS.toNanos(10L)); + .thenReturn(TimeUnit.SECONDS.toNanos(2L), TimeUnit.SECONDS.toNanos(20L)); lrsClient = new LoadReportClient( - logId, TARGET_NAME, + loadStatsManager, channel, NODE, syncContext, fakeClock.getScheduledExecutorService(), backoffPolicyProvider, fakeClock.getStopwatchSupplier()); - lrsClient.startLoadReporting(callback); + lrsClient.startLoadReporting(); } @After @@ -199,71 +205,95 @@ public void tearDown() { } @Test - public void typicalWorkflow() { + public void periodicLoadReporting() { verify(mockLoadReportingService).streamLoadStats(lrsResponseObserverCaptor.capture()); StreamObserver responseObserver = lrsResponseObserverCaptor.getValue(); StreamObserver requestObserver = Iterables.getOnlyElement(lrsRequestObservers); - InOrder inOrder = inOrder(requestObserver, callback); + InOrder inOrder = inOrder(requestObserver); inOrder.verify(requestObserver).onNext(eq(buildInitialRequest())); - String cluster1 = "cluster-foo.googleapis.com"; - ClusterStats rawStats1 = generateClusterLoadStats(cluster1, null); - when(loadStatsStore1.generateLoadReport()).thenReturn(rawStats1); - lrsClient.addLoadStatsStore(cluster1, null, loadStatsStore1); + FakeLoadStatsStore loadStatsStore1 = + (FakeLoadStatsStore) loadStatsManager.addLoadStats(CLUSTER1, null); + loadStatsStore1.refresh(); // Management server asks to report loads for cluster1. - responseObserver.onNext(buildLrsResponse(ImmutableList.of(cluster1), 1000)); - inOrder.verify(callback).onReportResponse(1000); + responseObserver.onNext(buildLrsResponse(ImmutableList.of(CLUSTER1), 1000)); - ArgumentMatcher expectedLoadReportMatcher = - new LoadStatsRequestMatcher(ImmutableList.of(rawStats1), 1000); fakeClock.forwardNanos(999); inOrder.verifyNoMoreInteractions(); fakeClock.forwardNanos(1); - inOrder.verify(requestObserver).onNext(argThat(expectedLoadReportMatcher)); + assertThat(loadStatsStore1.reported).hasSize(1); + ClusterStats report1 = loadStatsStore1.reported.poll(); + assertThat(Durations.toNanos(report1.getLoadReportInterval())).isEqualTo(1000); + inOrder.verify(requestObserver) + .onNext(argThat(new LoadStatsRequestMatcher(Collections.singletonList(report1)))); + loadStatsStore1.refresh(); fakeClock.forwardNanos(1000); - inOrder.verify(requestObserver).onNext(argThat(expectedLoadReportMatcher)); + assertThat(loadStatsStore1.reported).hasSize(1); + report1 = loadStatsStore1.reported.poll(); + assertThat(Durations.toNanos(report1.getLoadReportInterval())).isEqualTo(1000); + inOrder.verify(requestObserver) + .onNext(argThat(new LoadStatsRequestMatcher(Collections.singletonList(report1)))); - String cluster2 = "cluster-bar.googleapis.com"; - ClusterStats rawStats2 = generateClusterLoadStats(cluster2, null); - when(loadStatsStore2.generateLoadReport()).thenReturn(rawStats2); - lrsClient.addLoadStatsStore(cluster2, null, loadStatsStore2); + FakeLoadStatsStore loadStatsStore2 = + (FakeLoadStatsStore) loadStatsManager.addLoadStats(CLUSTER2, null); + loadStatsStore2.refresh(); // Management server updates the interval of sending load reports, while still asking for // loads to cluster1 only. - responseObserver.onNext(buildLrsResponse(ImmutableList.of(cluster1), 2000)); - inOrder.verify(callback).onReportResponse(2000); + responseObserver.onNext(buildLrsResponse(ImmutableList.of(CLUSTER1), 2000)); fakeClock.forwardNanos(1000); inOrder.verifyNoMoreInteractions(); - fakeClock.forwardNanos(1000); + assertThat(loadStatsStore1.reported).hasSize(1); + report1 = loadStatsStore1.reported.poll(); + assertThat(Durations.toNanos(report1.getLoadReportInterval())).isEqualTo(2000); + assertThat(loadStatsStore2.reported).isEmpty(); inOrder.verify(requestObserver) - .onNext(argThat(new LoadStatsRequestMatcher(ImmutableList.of(rawStats1), 2000))); + .onNext(argThat(new LoadStatsRequestMatcher(Collections.singletonList(report1)))); - // Management server asks to report loads for cluster1 and cluster2. - responseObserver.onNext(buildLrsResponse(ImmutableList.of(cluster1, cluster2), 2000)); + // Management server asks to report loads for all clusters. + responseObserver.onNext( + LoadStatsResponse.newBuilder() + .setSendAllClusters(true) + .setLoadReportingInterval(Durations.fromNanos(2000)) + .build()); + loadStatsStore1.refresh(); + loadStatsStore2.refresh(); fakeClock.forwardNanos(2000); + assertThat(loadStatsStore1.reported).hasSize(1); + report1 = loadStatsStore1.reported.poll(); + assertThat(loadStatsStore2.reported).hasSize(1); + ClusterStats report2 = loadStatsStore2.reported.poll(); + assertThat(Durations.toNanos(report1.getLoadReportInterval())).isEqualTo(2000); + assertThat(Durations.toNanos(report2.getLoadReportInterval())).isEqualTo(2000 + 2000); inOrder.verify(requestObserver) - .onNext( - argThat( - new LoadStatsRequestMatcher(ImmutableList.of(rawStats1, rawStats2), 2000))); + .onNext(argThat(new LoadStatsRequestMatcher(Arrays.asList(report1, report2)))); // Load reports for cluster1 is no longer wanted. - responseObserver.onNext(buildLrsResponse(ImmutableList.of(cluster2), 2000)); + responseObserver.onNext(buildLrsResponse(Collections.singletonList(CLUSTER2), 2000)); + loadStatsStore1.refresh(); + loadStatsStore2.refresh(); fakeClock.forwardNanos(2000); + assertThat(loadStatsStore1.reported).isEmpty(); + assertThat(loadStatsStore2.reported).hasSize(1); + report2 = loadStatsStore2.reported.poll(); + assertThat(Durations.toNanos(report2.getLoadReportInterval())).isEqualTo(2000); inOrder.verify(requestObserver) - .onNext(argThat(new LoadStatsRequestMatcher(ImmutableList.of(rawStats2), 2000))); + .onNext(argThat(new LoadStatsRequestMatcher(Collections.singletonList(report2)))); // Management server asks loads for a cluster that client has no load data. responseObserver .onNext(buildLrsResponse(ImmutableList.of("cluster-unknown.googleapis.com"), 2000)); fakeClock.forwardNanos(2000); + assertThat(loadStatsStore1.reported).isEmpty(); + assertThat(loadStatsStore2.reported).isEmpty(); ArgumentCaptor reportCaptor = ArgumentCaptor.forClass(null); inOrder.verify(requestObserver).onNext(reportCaptor.capture()); assertThat(reportCaptor.getValue().getClusterStatsCount()).isEqualTo(0); @@ -282,9 +312,9 @@ public void lrsStreamClosedAndRetried() { String clusterName = "cluster-foo.googleapis.com"; String clusterServiceName = "service-blade.googleapis.com"; - ClusterStats stats = generateClusterLoadStats(clusterName, clusterServiceName); - when(loadStatsStore1.generateLoadReport()).thenReturn(stats); - lrsClient.addLoadStatsStore(clusterName, null, loadStatsStore1); + FakeLoadStatsStore loadStatsStore = + (FakeLoadStatsStore) loadStatsManager.addLoadStats(clusterName, clusterServiceName); + loadStatsStore.refresh(); // First balancer RPC verify(requestObserver).onNext(eq(buildInitialRequest())); @@ -331,7 +361,7 @@ public void lrsStreamClosedAndRetried() { // Balancer sends a response asking for loads of the cluster. responseObserver - .onNext(buildLrsResponse(ImmutableList.of(clusterName), 0)); + .onNext(buildLrsResponse(ImmutableList.of(clusterName), 5)); // Then breaks the RPC responseObserver.onError(Status.UNAVAILABLE.asException()); @@ -348,12 +378,12 @@ public void lrsStreamClosedAndRetried() { fakeClock.forwardNanos(4); responseObserver.onError(Status.UNAVAILABLE.asException()); - // Will be on the first retry (1s) of backoff sequence 2. + // Will be on the first retry (2s) of backoff sequence 2. inOrder.verify(backoffPolicy2).nextBackoffNanos(); assertEquals(1, fakeClock.numPendingTasks(LRS_RPC_RETRY_TASK_FILTER)); // Fast-forward to a moment before the retry, the time spent in the last try is deducted. - fakeClock.forwardNanos(TimeUnit.SECONDS.toNanos(1) - 4 - 1); + fakeClock.forwardNanos(TimeUnit.SECONDS.toNanos(2) - 4 - 1); verifyNoMoreInteractions(mockLoadReportingService); // Then time for retry fakeClock.forwardNanos(1); @@ -368,8 +398,11 @@ public void lrsStreamClosedAndRetried() { responseObserver .onNext(buildLrsResponse(ImmutableList.of(clusterName), 10)); fakeClock.forwardNanos(10); + ClusterStats report = Iterables.getOnlyElement(loadStatsStore.reported); + assertThat(Durations.toNanos(report.getLoadReportInterval())) + .isEqualTo(TimeUnit.SECONDS.toNanos(1 + 10 + 2) + 10); verify(requestObserver) - .onNext(argThat(new LoadStatsRequestMatcher(ImmutableList.of(stats), 10))); + .onNext(argThat(new LoadStatsRequestMatcher(Collections.singletonList(report)))); // Wrapping up verify(backoffPolicyProvider, times(2)).get(); @@ -386,9 +419,9 @@ public void raceBetweenLoadReportingAndLbStreamClosure() { String clusterName = "cluster-foo.googleapis.com"; String clusterServiceName = "service-blade.googleapis.com"; - ClusterStats stats = generateClusterLoadStats(clusterName, clusterServiceName); - when(loadStatsStore1.generateLoadReport()).thenReturn(stats); - lrsClient.addLoadStatsStore(clusterName, null, loadStatsStore1); + FakeLoadStatsStore loadStatsStore = + (FakeLoadStatsStore) loadStatsManager.addLoadStats(clusterName, clusterServiceName); + loadStatsStore.refresh(); // First balancer RPC verify(requestObserver).onNext(eq(buildInitialRequest())); @@ -445,59 +478,15 @@ private static LoadStatsRequest buildInitialRequest() { .build(); } - /** - * Generates a raw service load stats report with random data. - */ - private static ClusterStats generateClusterLoadStats( - String clusterName, @Nullable String clusterServiceName) { - long callsInProgress = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); - long callsSucceeded = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); - long callsFailed = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); - long callsIssued = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); - long numLbDrops = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); - long numThrottleDrops = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); - - ClusterStats.Builder clusterStatsBuilder = ClusterStats.newBuilder(); - clusterStatsBuilder.setClusterName(clusterName); - if (clusterServiceName != null) { - clusterStatsBuilder.setClusterServiceName(clusterServiceName); - } - clusterStatsBuilder.addUpstreamLocalityStats( - UpstreamLocalityStats.newBuilder() - .setLocality( - Locality.newBuilder() - .setRegion("region-foo") - .setZone("zone-bar") - .setSubZone("subzone-baz")) - .setTotalRequestsInProgress(callsInProgress) - .setTotalSuccessfulRequests(callsSucceeded) - .setTotalErrorRequests(callsFailed) - .setTotalIssuedRequests(callsIssued)) - .addDroppedRequests( - DroppedRequests.newBuilder() - .setCategory("lb") - .setDroppedCount(numLbDrops)) - .addDroppedRequests( - DroppedRequests.newBuilder() - .setCategory("throttle") - .setDroppedCount(numThrottleDrops)) - .setTotalDroppedRequests(numLbDrops + numThrottleDrops); - return clusterStatsBuilder.build(); - } - /** * For comparing LoadStatsRequest stats data regardless of . */ private static class LoadStatsRequestMatcher implements ArgumentMatcher { private final Map expectedStats = new HashMap<>(); - LoadStatsRequestMatcher(Collection clusterStats, long expectedIntervalNano) { + LoadStatsRequestMatcher(Collection clusterStats) { for (ClusterStats stats : clusterStats) { - ClusterStats statsWithInterval = - stats.toBuilder() - .setLoadReportInterval(Durations.fromNanos(expectedIntervalNano)) - .build(); - expectedStats.put(statsWithInterval.getClusterName(), statsWithInterval); + expectedStats.put(stats.getClusterName(), stats); } } @@ -519,4 +508,82 @@ public boolean matches(LoadStatsRequest argument) { return true; } } + + private static final class FakeLoadStatsStore implements LoadStatsStore { + private final String cluster; + private final String clusterService; + private final Stopwatch stopwatch; + private final Queue reported = new ArrayDeque<>(); + private ClusterStats stats; + + private FakeLoadStatsStore(String cluster, String clusterService, Stopwatch stopwatch) { + this.cluster = cluster; + this.clusterService = clusterService; + this.stopwatch = stopwatch; + stopwatch.reset().start(); + refresh(); + } + + @Override + public ClusterStats generateLoadReport() { + ClusterStats report = + stats.toBuilder() + .setLoadReportInterval(Durations.fromNanos(stopwatch.elapsed(TimeUnit.NANOSECONDS))) + .build(); + stopwatch.reset().start(); + reported.offer(report); + return report; + } + + @Override + public ClientLoadCounter addLocality(EnvoyProtoData.Locality locality) { + throw new UnsupportedOperationException("should not used"); + } + + @Override + public void removeLocality(EnvoyProtoData.Locality locality) { + throw new UnsupportedOperationException("should not used"); + } + + @Override + public void recordDroppedRequest(String category) { + throw new UnsupportedOperationException("should not used"); + } + + private void refresh() { + long callsInProgress = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); + long callsSucceeded = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); + long callsFailed = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); + long callsIssued = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); + long numLbDrops = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); + long numThrottleDrops = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE); + + ClusterStats.Builder clusterStatsBuilder = ClusterStats.newBuilder(); + clusterStatsBuilder.setClusterName(cluster); + if (clusterService != null) { + clusterStatsBuilder.setClusterServiceName(clusterService); + } + clusterStatsBuilder.addUpstreamLocalityStats( + UpstreamLocalityStats.newBuilder() + .setLocality( + Locality.newBuilder() + .setRegion(cluster + "-region-foo") + .setZone(cluster + "-zone-bar") + .setSubZone(cluster + "-subzone-baz")) + .setTotalRequestsInProgress(callsInProgress) + .setTotalSuccessfulRequests(callsSucceeded) + .setTotalErrorRequests(callsFailed) + .setTotalIssuedRequests(callsIssued)) + .addDroppedRequests( + DroppedRequests.newBuilder() + .setCategory("lb") + .setDroppedCount(numLbDrops)) + .addDroppedRequests( + DroppedRequests.newBuilder() + .setCategory("throttle") + .setDroppedCount(numThrottleDrops)) + .setTotalDroppedRequests(numLbDrops + numThrottleDrops); + stats = clusterStatsBuilder.build(); + } + } } diff --git a/xds/src/test/java/io/grpc/xds/LoadStatsStoreImplTest.java b/xds/src/test/java/io/grpc/xds/LoadStatsStoreImplTest.java index d58fd4bb92e..4320ff279fd 100644 --- a/xds/src/test/java/io/grpc/xds/LoadStatsStoreImplTest.java +++ b/xds/src/test/java/io/grpc/xds/LoadStatsStoreImplTest.java @@ -18,13 +18,17 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableMap; +import com.google.protobuf.util.Durations; import io.envoyproxy.envoy.api.v2.endpoint.ClusterStats; import io.envoyproxy.envoy.api.v2.endpoint.ClusterStats.DroppedRequests; import io.envoyproxy.envoy.api.v2.endpoint.EndpointLoadMetricStats; import io.envoyproxy.envoy.api.v2.endpoint.UpstreamLocalityStats; +import io.grpc.internal.FakeClock; import io.grpc.xds.ClientLoadCounter.MetricValue; import io.grpc.xds.EnvoyProtoData.Locality; +import io.grpc.xds.LoadStatsManager.LoadStatsStore; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -35,7 +39,6 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; import javax.annotation.Nullable; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -49,16 +52,15 @@ public class LoadStatsStoreImplTest { new Locality("test_region1", "test_zone", "test_subzone"); private static final Locality LOCALITY2 = new Locality("test_region2", "test_zone", "test_subzone"); - private ConcurrentMap localityLoadCounters; + private final FakeClock fakeClock = new FakeClock(); private ConcurrentMap dropCounters; private LoadStatsStore loadStatsStore; @Before public void setUp() { - localityLoadCounters = new ConcurrentHashMap<>(); dropCounters = new ConcurrentHashMap<>(); - loadStatsStore = - new LoadStatsStoreImpl(CLUSTER_NAME, null, localityLoadCounters, dropCounters); + Stopwatch stopwatch = fakeClock.getStopwatchSupplier().get(); + loadStatsStore = new LoadStatsStoreImpl(CLUSTER_NAME, null, stopwatch, dropCounters); } private static List buildEndpointLoadMetricStatsList( @@ -83,7 +85,7 @@ private static UpstreamLocalityStats buildUpstreamLocalityStats( @Nullable List metrics) { UpstreamLocalityStats.Builder builder = UpstreamLocalityStats.newBuilder() - .setLocality(locality.toEnvoyProtoLocality()) + .setLocality(locality.toEnvoyProtoLocalityV2()) .setTotalSuccessfulRequests(callsSucceed) .setTotalErrorRequests(callsFailed) .setTotalRequestsInProgress(callsInProgress) @@ -103,7 +105,7 @@ private static DroppedRequests buildDroppedRequests(String category, long counts private static ClusterStats buildClusterStats( @Nullable List upstreamLocalityStatsList, - @Nullable List droppedRequestsList) { + @Nullable List droppedRequestsList, long intervalNano) { ClusterStats.Builder clusterStatsBuilder = ClusterStats.newBuilder(); clusterStatsBuilder.setClusterName(CLUSTER_NAME); if (upstreamLocalityStatsList != null) { @@ -117,6 +119,7 @@ private static ClusterStats buildClusterStats( } clusterStatsBuilder.setTotalDroppedRequests(dropCount); } + clusterStatsBuilder.setLoadReportInterval(Durations.fromNanos(intervalNano)); return clusterStatsBuilder.build(); } @@ -159,74 +162,47 @@ private static void assertUpstreamLocalityStatsEqual(UpstreamLocalityStats expec } @Test - public void addAndGetAndRemoveLocality() { - loadStatsStore.addLocality(LOCALITY1); - assertThat(localityLoadCounters).containsKey(LOCALITY1); - - // Adding the same locality counter again causes an exception. - try { - loadStatsStore.addLocality(LOCALITY1); - Assert.fail(); - } catch (IllegalStateException expected) { - assertThat(expected).hasMessageThat() - .contains("An active counter for locality " + LOCALITY1 + " already exists"); - } - - assertThat(loadStatsStore.getLocalityCounter(LOCALITY1)) - .isSameInstanceAs(localityLoadCounters.get(LOCALITY1)); - assertThat(loadStatsStore.getLocalityCounter(LOCALITY2)).isNull(); - - // Removing an non-existing locality counter causes an exception. - try { - loadStatsStore.removeLocality(LOCALITY2); - Assert.fail(); - } catch (IllegalStateException expected) { - assertThat(expected).hasMessageThat() - .contains("No active counter for locality " + LOCALITY2 + " exists"); - } - - // Removing the locality counter only mark it as inactive, but not throw it away. - loadStatsStore.removeLocality(LOCALITY1); - assertThat(localityLoadCounters.get(LOCALITY1).isActive()).isFalse(); - - // Removing an inactive locality counter causes an exception. - try { - loadStatsStore.removeLocality(LOCALITY1); - Assert.fail(); - } catch (IllegalStateException expected) { - assertThat(expected).hasMessageThat() - .contains("No active counter for locality " + LOCALITY1 + " exists"); - } - - // Adding it back simply mark it as active again. + public void removeInactiveCountersAfterGeneratingLoadReport() { loadStatsStore.addLocality(LOCALITY1); - assertThat(localityLoadCounters.get(LOCALITY1).isActive()).isTrue(); + assertThat(loadStatsStore.generateLoadReport().getUpstreamLocalityStatsCount()).isEqualTo(1); + loadStatsStore.removeLocality(LOCALITY1); // becomes inactive + assertThat(loadStatsStore.generateLoadReport().getUpstreamLocalityStatsCount()).isEqualTo(1); + assertThat(loadStatsStore.generateLoadReport().getUpstreamLocalityStatsCount()).isEqualTo(0); } @Test - public void removeInactiveCountersAfterGeneratingLoadReport() { - localityLoadCounters.put(LOCALITY1, new ClientLoadCounter()); - ClientLoadCounter inactiveCounter = new ClientLoadCounter(); - inactiveCounter.setActive(false); - localityLoadCounters.put(LOCALITY2, inactiveCounter); - loadStatsStore.generateLoadReport(); - assertThat(localityLoadCounters).containsKey(LOCALITY1); - assertThat(localityLoadCounters).doesNotContainKey(LOCALITY2); + public void localityCountersReferenceCounted() { + loadStatsStore.addLocality(LOCALITY1); + loadStatsStore.addLocality(LOCALITY1); + loadStatsStore.removeLocality(LOCALITY1); + assertThat(loadStatsStore.generateLoadReport().getUpstreamLocalityStatsCount()).isEqualTo(1); + assertThat(loadStatsStore.generateLoadReport().getUpstreamLocalityStatsCount()) + .isEqualTo(1); // still active + loadStatsStore.removeLocality(LOCALITY1); // becomes inactive + assertThat(loadStatsStore.generateLoadReport().getUpstreamLocalityStatsCount()).isEqualTo(1); + assertThat(loadStatsStore.generateLoadReport().getUpstreamLocalityStatsCount()).isEqualTo(0); } @Test public void loadReportContainsRecordedStats() { - ClientLoadCounter counter1 = new ClientLoadCounter(4315, 3421, 23, 593); + ClientLoadCounter counter1 = loadStatsStore.addLocality(LOCALITY1); + counter1.setCallsSucceeded(4315); + counter1.setCallsInProgress(3421); + counter1.setCallsFailed(23); + counter1.setCallsIssued(593); counter1.recordMetric("cpu_utilization", 0.3244); counter1.recordMetric("mem_utilization", 0.01233); counter1.recordMetric("named_cost_or_utilization", 3221.6543); - ClientLoadCounter counter2 = new ClientLoadCounter(41234, 432, 431, 702); + ClientLoadCounter counter2 = loadStatsStore.addLocality(LOCALITY2); + counter2.setCallsSucceeded(41234); + counter2.setCallsInProgress(432); + counter2.setCallsFailed(431); + counter2.setCallsIssued(702); counter2.recordMetric("cpu_utilization", 0.6526); counter2.recordMetric("mem_utilization", 0.3473); counter2.recordMetric("named_cost_or_utilization", 87653.4234); - localityLoadCounters.put(LOCALITY1, counter1); - localityLoadCounters.put(LOCALITY2, counter2); + fakeClock.forwardNanos(1000L); Map metrics1 = ImmutableMap.of( "cpu_utilization", new MetricValue(1, 0.3244), @@ -245,16 +221,17 @@ public void loadReportContainsRecordedStats() { buildUpstreamLocalityStats(LOCALITY2, 41234, 432, 431, 702, buildEndpointLoadMetricStatsList(metrics2)) ), - null); + null, 1000L); assertClusterStatsEqual(expectedReport, loadStatsStore.generateLoadReport()); + fakeClock.forwardNanos(2000L); expectedReport = buildClusterStats( Arrays.asList( buildUpstreamLocalityStats(LOCALITY1, 0, 3421, 0, 0, null), buildUpstreamLocalityStats(LOCALITY2, 0, 432, 0, 0, null) ), - null); + null, 2000L); assertClusterStatsEqual(expectedReport, loadStatsStore.generateLoadReport()); } @@ -270,10 +247,13 @@ public void recordingDroppedRequests() { } assertThat(dropCounters.get("lb").get()).isEqualTo(numLbDrop); assertThat(dropCounters.get("throttle").get()).isEqualTo(numThrottleDrop); + + fakeClock.forwardNanos(1000L); ClusterStats expectedLoadReport = buildClusterStats(null, Arrays.asList(buildDroppedRequests("lb", numLbDrop), - buildDroppedRequests("throttle", numThrottleDrop))); + buildDroppedRequests("throttle", numThrottleDrop)), + 1000L); assertClusterStatsEqual(expectedLoadReport, loadStatsStore.generateLoadReport()); assertThat(dropCounters.get("lb").get()).isEqualTo(0); assertThat(dropCounters.get("throttle").get()).isEqualTo(0); diff --git a/xds/src/test/java/io/grpc/xds/LocalityStoreTest.java b/xds/src/test/java/io/grpc/xds/LocalityStoreTest.java index 4f9c8f51753..6c079b54edb 100644 --- a/xds/src/test/java/io/grpc/xds/LocalityStoreTest.java +++ b/xds/src/test/java/io/grpc/xds/LocalityStoreTest.java @@ -69,6 +69,7 @@ import io.grpc.xds.EnvoyProtoData.LbEndpoint; import io.grpc.xds.EnvoyProtoData.Locality; import io.grpc.xds.EnvoyProtoData.LocalityLbEndpoints; +import io.grpc.xds.LoadStatsManager.LoadStatsStore; import io.grpc.xds.LocalityStore.LocalityStoreImpl; import io.grpc.xds.OrcaOobUtil.OrcaOobReportListener; import io.grpc.xds.OrcaOobUtil.OrcaReportingConfig; @@ -84,7 +85,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -1053,8 +1053,8 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { ArgumentCaptor subchannelPickerCaptor = ArgumentCaptor.forClass(null); inOrder.verify(helper).updateBalancingState(same(READY), subchannelPickerCaptor.capture()); assertThat(subchannelPickerCaptor.getValue() - .pickSubchannel(mock(PickSubchannelArgs.class)) - .getSubchannel()) + .pickSubchannel(mock(PickSubchannelArgs.class)) + .getSubchannel()) .isSameInstanceAs(mockSubchannel3); // P0 gets READY - P0 R, P1 F&D, P2 R&D, P3 N/A @@ -1078,8 +1078,8 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { assertThat(deactivationTasks).hasSize(2); inOrder.verify(helper).updateBalancingState(same(READY), subchannelPickerCaptor.capture()); assertThat(subchannelPickerCaptor.getValue() - .pickSubchannel(mock(PickSubchannelArgs.class)) - .getSubchannel()) + .pickSubchannel(mock(PickSubchannelArgs.class)) + .getSubchannel()) .isSameInstanceAs(mockSubchannel1); // P1 gets READY - P0 R, P1 R&D, P2 R&D, P3 N/A @@ -1188,8 +1188,8 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { assertThat(fakeClock.getPendingTasks(deactivationTaskFilter)).hasSize(2); inOrder.verify(helper).updateBalancingState(same(READY), subchannelPickerCaptor.capture()); assertThat(subchannelPickerCaptor.getValue() - .pickSubchannel(mock(PickSubchannelArgs.class)) - .getSubchannel()) + .pickSubchannel(mock(PickSubchannelArgs.class)) + .getSubchannel()) .isSameInstanceAs(mockSubchannel22); // EDS update, localities moved: P0 sz1, sz3; P1 sz4; P2 sz2 - P0 C, P1 R, P2 R&D @@ -1214,8 +1214,8 @@ public void run() { assertThat(loadBalancers.values()).containsExactly(lb1, lb2, lb3, lb4); inOrder.verify(helper).updateBalancingState(same(READY), subchannelPickerCaptor.capture()); assertThat(subchannelPickerCaptor.getValue() - .pickSubchannel(mock(PickSubchannelArgs.class)) - .getSubchannel()) + .pickSubchannel(mock(PickSubchannelArgs.class)) + .getSubchannel()) .isSameInstanceAs(mockSubchannel4); // The order of the following four handleResolvedAddresses() does not matter. We want to verify // they are after helper.updateBalancingState() @@ -1283,8 +1283,8 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { assertThat(fakeClock.getPendingTasks(deactivationTaskFilter)).hasSize(1); inOrder.verify(helper).updateBalancingState(same(READY), subchannelPickerCaptor.capture()); assertThat(subchannelPickerCaptor.getValue() - .pickSubchannel(mock(PickSubchannelArgs.class)) - .getSubchannel()) + .pickSubchannel(mock(PickSubchannelArgs.class)) + .getSubchannel()) .isSameInstanceAs(newMockSubchannel3); inOrder.verifyNoMoreInteractions(); @@ -1337,9 +1337,11 @@ public ClusterStats generateLoadReport() { } @Override - public void addLocality(Locality locality) { + public ClientLoadCounter addLocality(Locality locality) { assertThat(localityCounters).doesNotContainKey(locality); - localityCounters.put(locality, new ClientLoadCounter()); + ClientLoadCounter counter = new ClientLoadCounter(); + localityCounters.put(locality, counter); + return counter; } @Override @@ -1348,12 +1350,6 @@ public void removeLocality(Locality locality) { localityCounters.remove(locality); } - @Nullable - @Override - public ClientLoadCounter getLocalityCounter(Locality locality) { - return localityCounters.get(locality); - } - @Override public void recordDroppedRequest(String category) { // NO-OP, verify by invocations. diff --git a/xds/src/test/java/io/grpc/xds/LrsLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/LrsLoadBalancerTest.java index e845a2ccc97..bea3a2f2faa 100644 --- a/xds/src/test/java/io/grpc/xds/LrsLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/LrsLoadBalancerTest.java @@ -40,6 +40,7 @@ import io.grpc.xds.ClientLoadCounter.LoadRecordingStreamTracerFactory; import io.grpc.xds.ClientLoadCounter.LoadRecordingSubchannelPicker; import io.grpc.xds.EnvoyProtoData.Locality; +import io.grpc.xds.LoadStatsManager.LoadStatsStore; import io.grpc.xds.LrsLoadBalancerProvider.LrsConfig; import java.net.SocketAddress; import java.util.ArrayDeque; @@ -72,8 +73,6 @@ public class LrsLoadBalancerTest { private static final String LRS_SERVER_NAME = "trafficdirector.googleapis.com"; private static final Locality TEST_LOCALITY = new Locality("test-region", "test-zone", "test-subzone"); - - private final ClientLoadCounter counter = new ClientLoadCounter(); private final LoadRecorder loadRecorder = new LoadRecorder(); private final Queue childBalancers = new ArrayDeque<>(); @@ -106,7 +105,7 @@ public void subchannelPickerInterceptedWithLoadRecording() { PickResult result = picker.pickSubchannel(mock(PickSubchannelArgs.class)); ClientStreamTracer.Factory tracerFactory = result.getStreamTracerFactory(); assertThat(((LoadRecordingStreamTracerFactory) tracerFactory).getCounter()) - .isSameInstanceAs(counter); + .isSameInstanceAs(loadRecorder.counter); loadBalancer.shutdown(); assertThat(childBalancer.shutdown).isTrue(); assertThat(loadRecorder.recording).isFalse(); @@ -300,7 +299,8 @@ public int hashCode() { } } - private final class LoadRecorder implements LoadStatsStore { + private static final class LoadRecorder implements LoadStatsStore { + private final ClientLoadCounter counter = new ClientLoadCounter(); private boolean recording = false; @Override @@ -309,9 +309,10 @@ public ClusterStats generateLoadReport() { } @Override - public void addLocality(Locality locality) { + public ClientLoadCounter addLocality(Locality locality) { assertThat(locality).isEqualTo(TEST_LOCALITY); recording = true; + return counter; } @Override @@ -320,12 +321,6 @@ public void removeLocality(Locality locality) { recording = false; } - @Override - public ClientLoadCounter getLocalityCounter(Locality locality) { - assertThat(locality).isEqualTo(TEST_LOCALITY); - return counter; - } - @Override public void recordDroppedRequest(String category) { throw new UnsupportedOperationException("should not be called"); diff --git a/xds/src/test/java/io/grpc/xds/XdsClientImplTest.java b/xds/src/test/java/io/grpc/xds/XdsClientImplTest.java index 8e8002ecc0b..ec81022c924 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientImplTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientImplTest.java @@ -39,7 +39,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; @@ -51,13 +50,10 @@ import com.google.protobuf.util.Durations; import io.envoyproxy.envoy.api.v2.ClusterLoadAssignment; import io.envoyproxy.envoy.api.v2.ClusterLoadAssignment.Policy; -import io.envoyproxy.envoy.api.v2.DiscoveryRequest; -import io.envoyproxy.envoy.api.v2.DiscoveryResponse; import io.envoyproxy.envoy.api.v2.auth.UpstreamTlsContext; import io.envoyproxy.envoy.api.v2.core.AggregatedConfigSource; import io.envoyproxy.envoy.api.v2.core.ConfigSource; import io.envoyproxy.envoy.api.v2.core.HealthStatus; -import io.envoyproxy.envoy.api.v2.core.Node; import io.envoyproxy.envoy.api.v2.endpoint.ClusterStats; import io.envoyproxy.envoy.api.v2.route.RedirectAction; import io.envoyproxy.envoy.api.v2.route.WeightedCluster; @@ -70,7 +66,9 @@ import io.envoyproxy.envoy.config.route.v3.RouteMatch; import io.envoyproxy.envoy.config.route.v3.VirtualHost; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.SdsSecretConfig; -import io.envoyproxy.envoy.service.discovery.v2.AggregatedDiscoveryServiceGrpc.AggregatedDiscoveryServiceImplBase; +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.v2.LoadReportingServiceGrpc.LoadReportingServiceImplBase; import io.envoyproxy.envoy.service.load_stats.v2.LoadStatsRequest; import io.envoyproxy.envoy.service.load_stats.v2.LoadStatsResponse; @@ -94,12 +92,14 @@ 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.ClusterUpdate; import io.grpc.xds.XdsClient.ClusterWatcher; import io.grpc.xds.XdsClient.ConfigUpdate; import io.grpc.xds.XdsClient.ConfigWatcher; import io.grpc.xds.XdsClient.EndpointUpdate; import io.grpc.xds.XdsClient.EndpointWatcher; +import io.grpc.xds.XdsClient.XdsChannel; import io.grpc.xds.XdsClient.XdsChannelFactory; import io.grpc.xds.XdsClientImpl.MessagePrinter; import java.io.IOException; @@ -125,14 +125,15 @@ import org.mockito.MockitoAnnotations; /** - * Tests for {@link XdsClientImpl}. + * 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.getDefaultInstance(); + private static final Node NODE = Node.newBuilder().build(); private static final FakeClock.TaskFilter RPC_RETRY_TASK_FILTER = new FakeClock.TaskFilter() { @Override @@ -279,13 +280,14 @@ public void cancelled(Context context) { cleanupRule.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()); List servers = - ImmutableList.of(new ServerInfo(serverName, ImmutableList.of())); + ImmutableList.of(new ServerInfo(serverName, ImmutableList.of(), null)); XdsChannelFactory channelFactory = new XdsChannelFactory() { @Override - ManagedChannel createChannel(List servers) { - assertThat(Iterables.getOnlyElement(servers).getServerUri()).isEqualTo(serverName); - assertThat(Iterables.getOnlyElement(servers).getChannelCredentials()).isEmpty(); - return channel; + XdsChannel createChannel(List servers) { + ServerInfo serverInfo = Iterables.getOnlyElement(servers); + assertThat(serverInfo.getServerUri()).isEqualTo(serverName); + assertThat(serverInfo.getChannelCredentials()).isEmpty(); + return new XdsChannel(channel, /* useProtocolV3= */ true); } }; @@ -294,7 +296,7 @@ ManagedChannel createChannel(List servers) { TARGET_AUTHORITY, servers, channelFactory, - NODE, + EnvoyProtoData.Node.newBuilder().build(), syncContext, fakeClock.getScheduledExecutorService(), backoffPolicyProvider, @@ -335,7 +337,7 @@ public void ldsResponseWithoutMatchingResource() { // 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_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); @@ -365,7 +367,7 @@ public void ldsResponseWithoutMatchingResource() { // Client sends an ACK LDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_LDS, "0000"))); verify(configWatcher, never()).onConfigChanged(any(ConfigUpdate.class)); verify(configWatcher, never()).onResourceDoesNotExist(TARGET_AUTHORITY); @@ -391,7 +393,7 @@ public void failToFindVirtualHostInLdsResponseInLineRouteConfig() { // 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_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); io.envoyproxy.envoy.api.v2.RouteConfiguration routeConfig = @@ -414,7 +416,7 @@ public void failToFindVirtualHostInLdsResponseInLineRouteConfig() { verify(requestObserver) .onNext( argThat(new DiscoveryRequestMatcher("", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_LDS, "0000"))); verify(configWatcher, never()).onConfigChanged(any(ConfigUpdate.class)); verify(configWatcher, never()).onResourceDoesNotExist(TARGET_AUTHORITY); @@ -440,7 +442,7 @@ public void resolveVirtualHostInLdsResponse() { // 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_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); ScheduledTask ldsRespTimer = Iterables.getOnlyElement( fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); @@ -487,7 +489,7 @@ public void resolveVirtualHostInLdsResponse() { // Client sends an ACK request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_LDS, "0000"))); ArgumentCaptor configUpdateCaptor = ArgumentCaptor.forClass(null); verify(configWatcher).onConfigChanged(configUpdateCaptor.capture()); @@ -513,7 +515,7 @@ public void rdsResponseWithoutMatchingResource() { // 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_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); Rds rdsConfig = Rds.newBuilder() @@ -533,12 +535,12 @@ public void rdsResponseWithoutMatchingResource() { // Client sends an ACK LDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + 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_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_RDS, ""))); assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); @@ -565,7 +567,7 @@ public void rdsResponseWithoutMatchingResource() { // Client sends an ACK RDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_RDS, "0000"))); verify(configWatcher, never()).onConfigChanged(any(ConfigUpdate.class)); verify(configWatcher, never()).onResourceDoesNotExist(anyString()); @@ -631,7 +633,7 @@ public void resolveVirtualHostInRdsResponse() { // Client sent an ACK RDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_RDS, "0000"))); ArgumentCaptor configUpdateCaptor = ArgumentCaptor.forClass(null); verify(configWatcher).onConfigChanged(configUpdateCaptor.capture()); @@ -736,7 +738,7 @@ public void resolveVirtualHostWithPathMatchingInRdsResponse() { // Client sent an ACK RDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_RDS, "0000"))); ArgumentCaptor configUpdateCaptor = ArgumentCaptor.forClass(null); verify(configWatcher).onConfigChanged(configUpdateCaptor.capture()); @@ -748,7 +750,8 @@ public void resolveVirtualHostWithPathMatchingInRdsResponse() { new io.grpc.xds.RouteMatch( /* prefix= */ null, /* path= */ "/service1/method1"), - new EnvoyProtoData.RouteAction("cl1.googleapis.com", null))); + 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 @@ -756,6 +759,7 @@ public void resolveVirtualHostWithPathMatchingInRdsResponse() { /* prefix= */ null, /* path= */ "/service2/method2"), new EnvoyProtoData.RouteAction( + TimeUnit.SECONDS.toNanos(15L), null, ImmutableList.of( new EnvoyProtoData.ClusterWeight("cl21.googleapis.com", 30), @@ -767,7 +771,8 @@ public void resolveVirtualHostWithPathMatchingInRdsResponse() { new io.grpc.xds.RouteMatch( /* prefix= */ "/service1/", /* path= */ null), - new EnvoyProtoData.RouteAction("cl1.googleapis.com", 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 @@ -775,7 +780,7 @@ public void resolveVirtualHostWithPathMatchingInRdsResponse() { /* prefix= */ "", /* path= */ null), new EnvoyProtoData.RouteAction( - "cluster.googleapis.com", null))); + TimeUnit.SECONDS.toNanos(15L), "cluster.googleapis.com", null))); } /** @@ -834,7 +839,7 @@ public void failToFindVirtualHostInRdsResponse() { verify(requestObserver) .onNext( argThat(new DiscoveryRequestMatcher("", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_RDS, "0000"))); verify(configWatcher, never()).onConfigChanged(any(ConfigUpdate.class)); verify(configWatcher, never()).onResourceDoesNotExist(anyString()); @@ -901,7 +906,7 @@ public void matchingVirtualHostDoesNotContainRouteAction() { verify(requestObserver) .onNext( argThat(new DiscoveryRequestMatcher("", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_RDS, "0000"))); verify(configWatcher, never()).onConfigChanged(any(ConfigUpdate.class)); verify(configWatcher, never()).onResourceDoesNotExist(anyString()); @@ -925,7 +930,7 @@ public void notifyUpdatedResources() { // 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_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); // Management server sends back an LDS response containing a RouteConfiguration for the // requested Listener directly in-line. @@ -950,7 +955,7 @@ public void notifyUpdatedResources() { // Client sends an ACK LDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_LDS, "0000"))); // Cluster name is resolved and notified to config watcher. ArgumentCaptor configUpdateCaptor = ArgumentCaptor.forClass(null); @@ -979,7 +984,7 @@ public void notifyUpdatedResources() { // Client sends an ACK LDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "1", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0001"))); + XdsClientImpl.ADS_TYPE_URL_LDS, "0001"))); // Updated cluster name is notified to config watcher. configUpdateCaptor = ArgumentCaptor.forClass(null); @@ -1008,12 +1013,12 @@ public void notifyUpdatedResources() { // Client sends an ACK LDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "2", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0002"))); + 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_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_RDS, ""))); // Management server sends back an RDS response containing the RouteConfiguration // for the requested resource. @@ -1032,7 +1037,7 @@ public void notifyUpdatedResources() { // Client sent an ACK RDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", "some-route-to-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_RDS, "0000"))); // Updated cluster name is notified to config watcher again. configUpdateCaptor = ArgumentCaptor.forClass(null); @@ -1055,7 +1060,7 @@ public void notifyUpdatedResources() { // Client sent an ACK RDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "1", "some-route-to-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0001"))); + XdsClientImpl.ADS_TYPE_URL_RDS, "0001"))); // Updated cluster name is notified to config watcher again. configUpdateCaptor = ArgumentCaptor.forClass(null); @@ -1091,7 +1096,7 @@ public void waitRdsResponsesForRequestedResource() { // 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_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); // Management sends back an LDS response telling client to do RDS. Rds rdsConfig = @@ -1113,12 +1118,12 @@ public void waitRdsResponsesForRequestedResource() { // Client sends an ACK LDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + 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_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_RDS, ""))); ScheduledTask rdsRespTimer = Iterables.getOnlyElement( @@ -1143,7 +1148,7 @@ public void waitRdsResponsesForRequestedResource() { // Client sent an ACK RDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_RDS, "0000"))); // Client waits for future RDS responses silently. verifyNoMoreInteractions(configWatcher); @@ -1170,7 +1175,7 @@ public void waitRdsResponsesForRequestedResource() { // Client sent an ACK RDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "1", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0001"))); + XdsClientImpl.ADS_TYPE_URL_RDS, "0001"))); // Updated cluster name is notified to config watcher. ArgumentCaptor configUpdateCaptor = ArgumentCaptor.forClass(null); @@ -1193,7 +1198,7 @@ public void routeConfigurationRemovedNotifiedToWatcher() { // 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_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); // Management sends back an LDS response telling client to do RDS. Rds rdsConfig = @@ -1215,12 +1220,12 @@ public void routeConfigurationRemovedNotifiedToWatcher() { // Client sends an ACK LDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + 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_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_RDS, ""))); // Management server sends back an RDS response containing RouteConfiguration requested. List routeConfigs = ImmutableList.of( @@ -1237,7 +1242,7 @@ public void routeConfigurationRemovedNotifiedToWatcher() { // Client sent an ACK RDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_RDS, "0000"))); // Resolved cluster name is notified to config watcher. ArgumentCaptor configUpdateCaptor = ArgumentCaptor.forClass(null); @@ -1255,7 +1260,7 @@ public void routeConfigurationRemovedNotifiedToWatcher() { // Client sent an ACK LDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "1", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0001"))); + XdsClientImpl.ADS_TYPE_URL_LDS, "0001"))); verify(configWatcher).onResourceDoesNotExist(TARGET_AUTHORITY); } @@ -1290,7 +1295,7 @@ public void updateRdsRequestResourceWhileInitialResourceFetchInProgress() { // Client sends an (first) RDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "route-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_RDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_RDS, ""))); ScheduledTask rdsRespTimer = Iterables.getOnlyElement( @@ -1319,7 +1324,7 @@ public void updateRdsRequestResourceWhileInitialResourceFetchInProgress() { // 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_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_RDS, ""))); assertThat(rdsRespTimer.isCancelled()).isTrue(); rdsRespTimer = @@ -1356,7 +1361,7 @@ public void cdsResponseWithoutMatchingResource() { // 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_V2, ""))); + 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. @@ -1370,7 +1375,7 @@ public void cdsResponseWithoutMatchingResource() { // Client sent an ACK CDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_CDS, "0000"))); verify(clusterWatcher, never()).onClusterChanged(any(ClusterUpdate.class)); verify(clusterWatcher, never()).onResourceDoesNotExist("cluster-foo.googleapis.com"); verify(clusterWatcher, never()).onError(any(Status.class)); @@ -1393,7 +1398,7 @@ public void cdsResponseWithMatchingResource() { // 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_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_CDS, ""))); ScheduledTask cdsRespTimer = Iterables.getOnlyElement( fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); @@ -1410,7 +1415,7 @@ public void cdsResponseWithMatchingResource() { // Client sent an ACK CDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_CDS, "0000"))); assertThat(cdsRespTimer.isCancelled()).isTrue(); ArgumentCaptor clusterUpdateCaptor = ArgumentCaptor.forClass(null); @@ -1434,7 +1439,7 @@ public void cdsResponseWithMatchingResource() { // Client sent an ACK CDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "1", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0001"))); + XdsClientImpl.ADS_TYPE_URL_CDS, "0001"))); verify(clusterWatcher, times(2)).onClusterChanged(clusterUpdateCaptor.capture()); clusterUpdate = clusterUpdateCaptor.getValue(); @@ -1469,7 +1474,7 @@ public void cdsResponseWithUpstreamTlsContext() { // Client sent an ACK CDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_CDS, "0000"))); ArgumentCaptor clusterUpdateCaptor = ArgumentCaptor.forClass(null); verify(clusterWatcher, times(1)).onClusterChanged(clusterUpdateCaptor.capture()); ClusterUpdate clusterUpdate = clusterUpdateCaptor.getValue(); @@ -1507,7 +1512,7 @@ public void multipleClusterWatchers() { argThat( new DiscoveryRequestMatcher("", ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + 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 @@ -1525,7 +1530,7 @@ public void multipleClusterWatchers() { argThat( new DiscoveryRequestMatcher("0", ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_CDS, "0000"))); // Two watchers get notification of cluster update for the cluster they are interested in. ArgumentCaptor clusterUpdateCaptor1 = ArgumentCaptor.forClass(null); @@ -1572,7 +1577,7 @@ public void multipleClusterWatchers() { argThat( new DiscoveryRequestMatcher("1", ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0001"))); + XdsClientImpl.ADS_TYPE_URL_CDS, "0001"))); verifyNoMoreInteractions(watcher1, watcher2); // resource has no change ArgumentCaptor clusterUpdateCaptor3 = ArgumentCaptor.forClass(null); @@ -1602,7 +1607,7 @@ public void watchClusterAlreadyBeingWatched() { // Client sends an CDS request to management server. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + 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 @@ -1616,7 +1621,7 @@ public void watchClusterAlreadyBeingWatched() { // Client sent an ACK CDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_CDS, "0000"))); ArgumentCaptor clusterUpdateCaptor1 = ArgumentCaptor.forClass(null); verify(watcher1).onClusterChanged(clusterUpdateCaptor1.capture()); @@ -1660,7 +1665,7 @@ public void addRemoveClusterWatchers() { // Client sends an CDS request to management server. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_CDS, ""))); // Management server sends back a CDS response with Cluster for the requested // cluster. @@ -1673,7 +1678,7 @@ public void addRemoveClusterWatchers() { // Client sent an ACK CDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_CDS, "0000"))); ArgumentCaptor clusterUpdateCaptor1 = ArgumentCaptor.forClass(null); verify(watcher1).onClusterChanged(clusterUpdateCaptor1.capture()); @@ -1693,7 +1698,7 @@ public void addRemoveClusterWatchers() { argThat( new DiscoveryRequestMatcher("0", ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_CDS, "0000"))); // Management server sends back a CDS response with Cluster for all requested cluster. clusters = ImmutableList.of( @@ -1711,7 +1716,7 @@ public void addRemoveClusterWatchers() { argThat( new DiscoveryRequestMatcher("1", ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0001"))); + XdsClientImpl.ADS_TYPE_URL_CDS, "0001"))); verifyNoMoreInteractions(watcher1); // resource has no change ArgumentCaptor clusterUpdateCaptor2 = ArgumentCaptor.forClass(null); verify(watcher2).onClusterChanged(clusterUpdateCaptor2.capture()); @@ -1730,7 +1735,7 @@ public void addRemoveClusterWatchers() { // that cluster. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "1", "cluster-bar.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0001"))); + XdsClientImpl.ADS_TYPE_URL_CDS, "0001"))); // Management server has nothing to respond. @@ -1741,7 +1746,7 @@ public void addRemoveClusterWatchers() { .onNext( argThat( new DiscoveryRequestMatcher("1", ImmutableList.of(), - XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0001"))); + XdsClientImpl.ADS_TYPE_URL_CDS, "0001"))); // Management server sends back a new CDS response. clusters = ImmutableList.of( @@ -1756,7 +1761,7 @@ public void addRemoveClusterWatchers() { .onNext( argThat( new DiscoveryRequestMatcher("2", ImmutableList.of(), - XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0002"))); + XdsClientImpl.ADS_TYPE_URL_CDS, "0002"))); // Cancelled watchers do not receive notification. verifyNoMoreInteractions(watcher1, watcher2); @@ -1769,7 +1774,7 @@ public void addRemoveClusterWatchers() { // 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_V2, "0002"))); + 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). @@ -1795,7 +1800,7 @@ public void addRemoveClusterWatchers() { // 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_V2, "0003"))); + XdsClientImpl.ADS_TYPE_URL_CDS, "0003"))); } @Test @@ -1811,7 +1816,7 @@ public void addRemoveClusterWatcherWhileInitialResourceFetchInProgress() { .onNext( argThat( new DiscoveryRequestMatcher("", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + 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); @@ -1829,7 +1834,7 @@ public void addRemoveClusterWatcherWhileInitialResourceFetchInProgress() { argThat( new DiscoveryRequestMatcher("", ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_CDS, ""))); assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(2); fakeClock.forwardTime(1, TimeUnit.SECONDS); @@ -1860,7 +1865,8 @@ public void addRemoveClusterWatcherWhileInitialResourceFetchInProgress() { assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); assertThat(timeoutTask.isCancelled()).isTrue(); - verifyZeroInteractions(watcher3, watcher4); + // TODO(chengyuanzhang): migrate to verifyNoInteractions. + verifyNoMoreInteractions(watcher3, watcher4); } @Test @@ -1871,7 +1877,7 @@ public void cdsUpdateForClusterBeingRemoved() { verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + 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. @@ -1917,7 +1923,7 @@ public void edsResponseWithoutMatchingResource() { // 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_V2, ""))); + 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 @@ -1946,7 +1952,7 @@ public void edsResponseWithoutMatchingResource() { // Client sent an ACK EDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_EDS, "0000"))); verify(endpointWatcher, never()).onEndpointChanged(any(EndpointUpdate.class)); verify(endpointWatcher, never()).onResourceDoesNotExist("cluster-foo.googleapis.com"); @@ -1969,7 +1975,7 @@ public void edsResponseWithMatchingResource() { // 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_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_EDS, ""))); ScheduledTask edsRespTimeoutTask = Iterables.getOnlyElement( fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)); @@ -2012,7 +2018,7 @@ public void edsResponseWithMatchingResource() { // Client sent an ACK EDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_EDS, "0000"))); ArgumentCaptor endpointUpdateCaptor = ArgumentCaptor.forClass(null); verify(endpointWatcher).onEndpointChanged(endpointUpdateCaptor.capture()); @@ -2045,7 +2051,7 @@ public void edsResponseWithMatchingResource() { // Client sent an ACK EDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "1", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0001"))); + XdsClientImpl.ADS_TYPE_URL_EDS, "0001"))); verify(endpointWatcher, times(2)).onEndpointChanged(endpointUpdateCaptor.capture()); endpointUpdate = endpointUpdateCaptor.getValue(); @@ -2072,7 +2078,7 @@ public void multipleEndpointWatchers() { argThat( new DiscoveryRequestMatcher("", ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_EDS, ""))); assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(2); @@ -2100,7 +2106,7 @@ public void multipleEndpointWatchers() { argThat( new DiscoveryRequestMatcher("0", ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_EDS, "0000"))); // Two watchers get notification of endpoint update for the cluster they are interested in. ArgumentCaptor endpointUpdateCaptor1 = ArgumentCaptor.forClass(null); @@ -2127,7 +2133,8 @@ public void multipleEndpointWatchers() { new LbEndpoint("192.168.0.1", 8080, 2, true)), 1, 0)); - verifyZeroInteractions(watcher3); + // TODO(chengyuanzhang): migrate to verifyNoInteractions. + verifyNoMoreInteractions(watcher3); // Management server sends back another EDS response contains ClusterLoadAssignment for the // other requested cluster. @@ -2150,7 +2157,7 @@ public void multipleEndpointWatchers() { argThat( new DiscoveryRequestMatcher("1", ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0001"))); + XdsClientImpl.ADS_TYPE_URL_EDS, "0001"))); // The corresponding watcher gets notified. ArgumentCaptor endpointUpdateCaptor3 = ArgumentCaptor.forClass(null); @@ -2181,7 +2188,7 @@ public void watchEndpointsForClusterAlreadyBeingWatched() { // Client sends first EDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_EDS, ""))); assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); @@ -2206,7 +2213,7 @@ public void watchEndpointsForClusterAlreadyBeingWatched() { // Client sent an ACK EDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_EDS, "0000"))); ArgumentCaptor endpointUpdateCaptor1 = ArgumentCaptor.forClass(null); verify(watcher1).onEndpointChanged(endpointUpdateCaptor1.capture()); @@ -2259,7 +2266,7 @@ public void addRemoveEndpointWatchers() { // Client sends an EDS request to management server. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_EDS, ""))); // Management server sends back an EDS response with ClusterLoadAssignment for the requested // cluster. @@ -2281,7 +2288,7 @@ public void addRemoveEndpointWatchers() { // Client sent an ACK EDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_EDS, "0000"))); ArgumentCaptor endpointUpdateCaptor1 = ArgumentCaptor.forClass(null); verify(watcher1).onEndpointChanged(endpointUpdateCaptor1.capture()); @@ -2306,7 +2313,7 @@ public void addRemoveEndpointWatchers() { argThat( new DiscoveryRequestMatcher("0", ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0000"))); + XdsClientImpl.ADS_TYPE_URL_EDS, "0000"))); // Management server sends back an EDS response with ClusterLoadAssignment for one of requested // cluster. @@ -2329,7 +2336,7 @@ public void addRemoveEndpointWatchers() { argThat( new DiscoveryRequestMatcher("1", ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0001"))); + XdsClientImpl.ADS_TYPE_URL_EDS, "0001"))); ArgumentCaptor endpointUpdateCaptor2 = ArgumentCaptor.forClass(null); verify(watcher2).onEndpointChanged(endpointUpdateCaptor2.capture()); @@ -2350,7 +2357,7 @@ public void addRemoveEndpointWatchers() { // 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_V2, "0001"))); + XdsClientImpl.ADS_TYPE_URL_EDS, "0001"))); // Management server should not respond as it had previously sent the requested resource. @@ -2364,7 +2371,7 @@ public void addRemoveEndpointWatchers() { argThat( new DiscoveryRequestMatcher("1", ImmutableList.of(), // empty resources - XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0001"))); + XdsClientImpl.ADS_TYPE_URL_EDS, "0001"))); // All endpoint watchers have been cancelled. @@ -2395,7 +2402,7 @@ public void addRemoveEndpointWatchers() { argThat( new DiscoveryRequestMatcher("2", ImmutableList.of(), // empty resources - XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0002"))); + XdsClientImpl.ADS_TYPE_URL_EDS, "0002"))); // Cancelled watchers do not receive notification. verifyNoMoreInteractions(watcher1, watcher2); @@ -2412,7 +2419,7 @@ public void addRemoveEndpointWatchers() { // 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_V2, "0002"))); + XdsClientImpl.ADS_TYPE_URL_EDS, "0002"))); // Management server sends back an EDS response for re-subscribed resource. clusterLoadAssignments = ImmutableList.of( @@ -2446,7 +2453,7 @@ public void addRemoveEndpointWatchers() { argThat( new DiscoveryRequestMatcher("3", ImmutableList.of("cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0003"))); + XdsClientImpl.ADS_TYPE_URL_EDS, "0003"))); } @Test @@ -2462,7 +2469,7 @@ public void addRemoveEndpointWatcherWhileInitialResourceFetchInProgress() { .onNext( argThat( new DiscoveryRequestMatcher("", "cluster-foo.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + 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); @@ -2480,7 +2487,7 @@ public void addRemoveEndpointWatcherWhileInitialResourceFetchInProgress() { argThat( new DiscoveryRequestMatcher("", ImmutableList.of("cluster-foo.googleapis.com", "cluster-bar.googleapis.com"), - XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_EDS, ""))); assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(2); fakeClock.forwardTime(1, TimeUnit.SECONDS); @@ -2511,7 +2518,8 @@ public void addRemoveEndpointWatcherWhileInitialResourceFetchInProgress() { assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); assertThat(timeoutTask.isCancelled()).isTrue(); - verifyZeroInteractions(watcher3, watcher4); + // TODO(chengyuanzhang): migrate to verifyNoInteractions. + verifyNoMoreInteractions(watcher3, watcher4); } @Test @@ -2589,7 +2597,7 @@ public void streamClosedAndRetryWhenResolvingConfig() { // 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_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); // Management server closes the RPC stream immediately. responseObserver.onCompleted(); @@ -2609,7 +2617,7 @@ public void streamClosedAndRetryWhenResolvingConfig() { // Client retried by sending an LDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); // Management server closes the RPC stream with an error. responseObserver.onError(Status.UNAVAILABLE.asException()); @@ -2629,7 +2637,7 @@ public void streamClosedAndRetryWhenResolvingConfig() { // Client retried again by sending an LDS. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); // Management server responses with a listener for the requested resource. Rds rdsConfig = @@ -2650,12 +2658,12 @@ public void streamClosedAndRetryWhenResolvingConfig() { // Client sent back an ACK LDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "0", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + 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_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_RDS, ""))); // Management server encounters an error and closes the stream. responseObserver.onError(Status.UNKNOWN.asException()); @@ -2669,7 +2677,7 @@ public void streamClosedAndRetryWhenResolvingConfig() { requestObserver = requestObservers.poll(); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); // RPC stream closed immediately responseObserver.onError(Status.UNKNOWN.asException()); @@ -2686,7 +2694,7 @@ public void streamClosedAndRetryWhenResolvingConfig() { requestObserver = requestObservers.poll(); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); // Management server sends an LDS response. responseObserver.onNext(ldsResponse); @@ -2721,7 +2729,7 @@ public void streamClosedAndRetryWhenResolvingConfig() { requestObserver = requestObservers.poll(); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); verifyNoMoreInteractions(backoffPolicyProvider, backoffPolicy1, backoffPolicy2); } @@ -2753,7 +2761,7 @@ public void streamClosedAndRetry() { // Client sent first CDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_CDS, ""))); // Start watching endpoint information. xdsClient.watchEndpointData("cluster.googleapis.com", endpointWatcher); @@ -2761,7 +2769,7 @@ public void streamClosedAndRetry() { // Client sent first EDS request. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_EDS, ""))); // Management server closes the RPC stream with an error. responseObserver.onError(Status.UNKNOWN.asException()); @@ -2783,13 +2791,13 @@ public void streamClosedAndRetry() { // Retry resumes requests for all wanted resources. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_CDS, ""))); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_EDS, ""))); // Management server becomes unreachable. responseObserver.onError(Status.UNAVAILABLE.asException()); @@ -2812,13 +2820,13 @@ public void streamClosedAndRetry() { requestObserver = requestObservers.poll(); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_CDS, ""))); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_EDS, ""))); // Management server is still not reachable. responseObserver.onError(Status.UNAVAILABLE.asException()); @@ -2841,13 +2849,13 @@ public void streamClosedAndRetry() { requestObserver = requestObservers.poll(); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_CDS, ""))); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_EDS, ""))); // Management server sends back a CDS response. List clusters = ImmutableList.of( @@ -2874,13 +2882,13 @@ public void streamClosedAndRetry() { verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_CDS, ""))); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_EDS, ""))); // Management server becomes unreachable again. responseObserver.onError(Status.UNAVAILABLE.asException()); @@ -2902,13 +2910,13 @@ public void streamClosedAndRetry() { requestObserver = requestObservers.poll(); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_CDS, ""))); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_EDS, ""))); verifyNoMoreInteractions(mockedDiscoveryService, backoffPolicyProvider, backoffPolicy1, backoffPolicy2); @@ -2947,7 +2955,7 @@ public void streamClosedAndRetryRaceWithAddingAndRemovingWatchers() { verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); // Management server becomes unreachable. responseObserver.onError(Status.UNAVAILABLE.asException()); @@ -2968,10 +2976,10 @@ public void streamClosedAndRetryRaceWithAddingAndRemovingWatchers() { verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_CDS, ""))); // Management server is still unreachable. responseObserver.onError(Status.UNAVAILABLE.asException()); @@ -2992,13 +3000,13 @@ public void streamClosedAndRetryRaceWithAddingAndRemovingWatchers() { verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_CDS, ""))); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_EDS, ""))); // Management server sends back a CDS response. List clusters = ImmutableList.of( @@ -3014,14 +3022,14 @@ public void streamClosedAndRetryRaceWithAddingAndRemovingWatchers() { // Client updates EDS resource subscription immediately. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", ImmutableList.of(), - XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_EDS, ""))); // Become interested in endpoints of another cluster. xdsClient.watchEndpointData("cluster2.googleapis.com", endpointWatcher); // Client updates EDS resource subscription immediately. verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster2.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_EDS, ""))); // Management server closes the RPC stream again. responseObserver.onCompleted(); @@ -3035,13 +3043,13 @@ public void streamClosedAndRetryRaceWithAddingAndRemovingWatchers() { requestObserver = requestObservers.poll(); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_CDS, ""))); verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster2.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_EDS, ""))); // Management server becomes unreachable again. responseObserver.onError(Status.UNAVAILABLE.asException()); @@ -3062,13 +3070,13 @@ public void streamClosedAndRetryRaceWithAddingAndRemovingWatchers() { verify(requestObserver) .onNext(eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); verify(requestObserver, never()) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_CDS, ""))); verify(requestObserver, never()) .onNext(eq(buildDiscoveryRequest(NODE, "", "cluster2.googleapis.com", - XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_EDS, ""))); verifyNoMoreInteractions(mockedDiscoveryService, backoffPolicyProvider, backoffPolicy1, backoffPolicy2); @@ -3134,7 +3142,7 @@ public void streamClosedAndRetryReschedulesAllResourceFetchTimer() { // Client resumed requests and management server sends back LDS resources again. verify(requestObserver).onNext( eq(buildDiscoveryRequest(NODE, "", TARGET_AUTHORITY, - XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + XdsClientImpl.ADS_TYPE_URL_LDS, ""))); responseObserver.onNext(response); // Client sent an RDS request for resource "route-foo.googleapis.com" (Omitted). @@ -3229,9 +3237,9 @@ public void streamClosedAndRetryReschedulesAllResourceFetchTimer() { @Test public void reportLoadStatsToServer() { String clusterName = "cluster-foo.googleapis.com"; - LoadStatsStore loadStatsStore = new LoadStatsStoreImpl(clusterName, null); + xdsClient.addClientStats(clusterName, null); ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(null); - xdsClient.reportClientStats(clusterName, null, loadStatsStore); + xdsClient.reportClientStats(); LoadReportCall lrsCall = loadReportCalls.poll(); verify(lrsCall.requestObserver).onNext(requestCaptor.capture()); assertThat(requestCaptor.getValue().getClusterStatsCount()) @@ -3247,12 +3255,14 @@ public void reportLoadStatsToServer() { ClusterStats report = Iterables.getOnlyElement(requestCaptor.getValue().getClusterStatsList()); assertThat(report.getClusterName()).isEqualTo(clusterName); - xdsClient.cancelClientStatsReport(clusterName, null); + 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 } @@ -3732,7 +3742,7 @@ public boolean matches(DiscoveryRequest argument) { if (!resourceNames.equals(new HashSet<>(argument.getResourceNamesList()))) { return false; } - return NODE.equals(argument.getNode()); + return argument.getNode().equals(NODE.toEnvoyProtoNode()); } } diff --git a/xds/src/test/java/io/grpc/xds/XdsClientImplTestForListener.java b/xds/src/test/java/io/grpc/xds/XdsClientImplTestForListener.java index fed260b7b11..6d1aeedaeb8 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientImplTestForListener.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientImplTestForListener.java @@ -17,7 +17,7 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; -import static io.grpc.xds.XdsClientTestHelper.buildDiscoveryResponse; +import static io.grpc.xds.XdsClientTestHelper.buildDiscoveryResponseV2; import static io.grpc.xds.XdsClientTestHelper.buildListener; import static io.grpc.xds.XdsClientTestHelper.buildRouteConfiguration; import static io.grpc.xds.XdsClientTestHelper.buildVirtualHost; @@ -37,16 +37,12 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.Any; import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.Struct; import com.google.protobuf.UInt32Value; -import com.google.protobuf.Value; import io.envoyproxy.envoy.api.v2.DiscoveryRequest; import io.envoyproxy.envoy.api.v2.DiscoveryResponse; import io.envoyproxy.envoy.api.v2.Listener; import io.envoyproxy.envoy.api.v2.auth.DownstreamTlsContext; -import io.envoyproxy.envoy.api.v2.core.Address; import io.envoyproxy.envoy.api.v2.core.CidrRange; -import io.envoyproxy.envoy.api.v2.core.Node; import io.envoyproxy.envoy.api.v2.core.SocketAddress; import io.envoyproxy.envoy.api.v2.core.TransportSocket; import io.envoyproxy.envoy.api.v2.listener.Filter; @@ -70,19 +66,23 @@ import io.grpc.testing.GrpcCleanupRule; import io.grpc.xds.Bootstrapper.ChannelCreds; import io.grpc.xds.Bootstrapper.ServerInfo; +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.XdsClient.XdsChannelFactory; import io.grpc.xds.internal.sds.CommonTlsContextTestsUtil; import java.io.IOException; import java.util.ArrayDeque; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Queue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; - import org.junit.After; import org.junit.Before; import org.junit.Ignore; @@ -109,7 +109,7 @@ public class XdsClientImplTestForListener { "type.googleapis.com/" + "envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"; - private static final Node NODE = Node.getDefaultInstance(); + private static final Node NODE = Node.newBuilder().build(); private static final FakeClock.TaskFilter RPC_RETRY_TASK_FILTER = new FakeClock.TaskFilter() { @Override @@ -201,13 +201,14 @@ public void cancelled(Context context) { cleanupRule.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()); List servers = - ImmutableList.of(new ServerInfo(serverName, ImmutableList.of())); + ImmutableList.of(new ServerInfo(serverName, ImmutableList.of(), null)); XdsChannelFactory channelFactory = new XdsChannelFactory() { @Override - ManagedChannel createChannel(List servers) { - assertThat(Iterables.getOnlyElement(servers).getServerUri()).isEqualTo(serverName); - assertThat(Iterables.getOnlyElement(servers).getChannelCredentials()).isEmpty(); - return channel; + XdsChannel createChannel(List servers) { + ServerInfo serverInfo = Iterables.getOnlyElement(servers); + assertThat(serverInfo.getServerUri()).isEqualTo(serverName); + assertThat(serverInfo.getChannelCredentials()).isEmpty(); + return new XdsChannel(channel, false); } }; @@ -230,15 +231,12 @@ public void tearDown() { } private static Node getNodeToVerify() { - Struct newMetadata = NODE.getMetadata().toBuilder() - .putFields("TRAFFICDIRECTOR_PROXYLESS", - Value.newBuilder().setStringValue("1").build()) - .build(); - Address listeningAddress = - Address.newBuilder() - .setSocketAddress( - SocketAddress.newBuilder().setAddress("0.0.0.0").setPortValue(PORT).build()) - .build(); + Map newMetadata = new HashMap<>(); + if (NODE.getMetadata() != null) { + newMetadata.putAll(NODE.getMetadata()); + } + newMetadata.put("TRAFFICDIRECTOR_PROXYLESS", "1"); + Address listeningAddress = new Address("0.0.0.0", PORT); return NODE.toBuilder() .setMetadata(newMetadata) .addListeningAddresses(listeningAddress) @@ -249,7 +247,7 @@ private static DiscoveryRequest buildDiscoveryRequest( Node node, String versionInfo, String typeUrl, String nonce) { return DiscoveryRequest.newBuilder() .setVersionInfo(versionInfo) - .setNode(node) + .setNode(node.toEnvoyProtoNodeV2()) .setTypeUrl(typeUrl) .setResponseNonce(nonce) .build(); @@ -332,7 +330,7 @@ public void ldsResponse_nonMatchingFilterChain_notFoundError() { "cluster-baz.googleapis.com")))) .build())))); DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); responseObserver.onNext(response); // Client sends an ACK LDS request. @@ -384,7 +382,7 @@ public void ldsResponseWith_listenerAddressPortMismatch() { filterChainInbound ))); DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); responseObserver.onNext(response); // Client sends an ACK LDS request. @@ -436,7 +434,7 @@ public void ldsResponseWith_matchingListenerFound() throws InvalidProtocolBuffer filterChainInbound ))); DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); responseObserver.onNext(response); // Client sends an ACK LDS request. @@ -511,7 +509,7 @@ public void notifyUpdatedListener() throws InvalidProtocolBufferException { filterChainInbound ))); DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); responseObserver.onNext(response); // Client sends an ACK LDS request. @@ -534,7 +532,7 @@ public void notifyUpdatedListener() throws InvalidProtocolBufferException { filterChainNewInbound ))); DiscoveryResponse response1 = - buildDiscoveryResponse("1", listeners1, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0001"); + buildDiscoveryResponseV2("1", listeners1, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0001"); responseObserver.onNext(response1); // Client sends an ACK LDS request. @@ -607,7 +605,7 @@ public void ldsResponse_nonMatchingIpAddress() { filterChainOutbound ))); DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); responseObserver.onNext(response); // Client sends an ACK LDS request. @@ -656,7 +654,7 @@ public void ldsResponse_nonMatchingPort() { filterChainOutbound ))); DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); responseObserver.onNext(response); // Client sends an ACK LDS request. @@ -707,7 +705,7 @@ public void streamClosedAndRetry() { filterChainInbound ))); DiscoveryResponse response = - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); responseObserver.onNext(response); ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(null); @@ -769,7 +767,7 @@ public void streamClosedAndRetry() { XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); // Management server sends back a LDS response. - response = buildDiscoveryResponse("1", listeners, + response = buildDiscoveryResponseV2("1", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0001"); responseObserver.onNext(response); @@ -815,10 +813,11 @@ public void streamClosedAndRetry() { static Listener buildListenerWithFilterChain(String name, int portValue, String address, FilterChain... filterChains) { - Address listenerAddress = Address.newBuilder() - .setSocketAddress(SocketAddress.newBuilder() - .setPortValue(portValue).setAddress(address)) - .build(); + io.envoyproxy.envoy.api.v2.core.Address listenerAddress = + io.envoyproxy.envoy.api.v2.core.Address.newBuilder() + .setSocketAddress( + SocketAddress.newBuilder().setPortValue(portValue).setAddress(address)) + .build(); return Listener.newBuilder() .setName(name) diff --git a/xds/src/test/java/io/grpc/xds/XdsClientImplTestV2.java b/xds/src/test/java/io/grpc/xds/XdsClientImplTestV2.java new file mode 100644 index 00000000000..fcd268e0193 --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/XdsClientImplTestV2.java @@ -0,0 +1,3770 @@ +/* + * 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.buildDiscoveryRequestV2; +import static io.grpc.xds.XdsClientTestHelper.buildDiscoveryResponseV2; +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.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.api.v2.ClusterLoadAssignment.Policy; +import io.envoyproxy.envoy.api.v2.DiscoveryRequest; +import io.envoyproxy.envoy.api.v2.DiscoveryResponse; +import io.envoyproxy.envoy.api.v2.auth.UpstreamTlsContext; +import io.envoyproxy.envoy.api.v2.core.AggregatedConfigSource; +import io.envoyproxy.envoy.api.v2.core.ConfigSource; +import io.envoyproxy.envoy.api.v2.core.HealthStatus; +import io.envoyproxy.envoy.api.v2.endpoint.ClusterStats; +import io.envoyproxy.envoy.api.v2.route.RedirectAction; +import io.envoyproxy.envoy.api.v2.route.WeightedCluster; +import io.envoyproxy.envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager; +import io.envoyproxy.envoy.config.filter.network.http_connection_manager.v2.Rds; +import io.envoyproxy.envoy.config.route.v3.QueryParameterMatcher; +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.extensions.transport_sockets.tls.v3.SdsSecretConfig; +import io.envoyproxy.envoy.service.discovery.v2.AggregatedDiscoveryServiceGrpc.AggregatedDiscoveryServiceImplBase; +import io.envoyproxy.envoy.service.load_stats.v2.LoadReportingServiceGrpc.LoadReportingServiceImplBase; +import io.envoyproxy.envoy.service.load_stats.v2.LoadStatsRequest; +import io.envoyproxy.envoy.service.load_stats.v2.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.Bootstrapper.ChannelCreds; +import io.grpc.xds.Bootstrapper.ServerInfo; +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.ClusterUpdate; +import io.grpc.xds.XdsClient.ClusterWatcher; +import io.grpc.xds.XdsClient.ConfigUpdate; +import io.grpc.xds.XdsClient.ConfigWatcher; +import io.grpc.xds.XdsClient.EndpointUpdate; +import io.grpc.xds.XdsClient.EndpointWatcher; +import io.grpc.xds.XdsClient.XdsChannel; +import io.grpc.xds.XdsClient.XdsChannelFactory; +import io.grpc.xds.XdsClientImpl.MessagePrinter; +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 v2 protocol. + */ +@RunWith(JUnit4.class) +public class XdsClientImplTestV2 { + + private static final String TARGET_AUTHORITY = "foo.googleapis.com:8080"; + + private static final Node NODE = Node.newBuilder().build(); + private static final TaskFilter RPC_RETRY_TASK_FILTER = + new TaskFilter() { + @Override + public boolean shouldAccept(Runnable command) { + return command.toString().contains(XdsClientImpl.RpcRetryTask.class.getSimpleName()); + } + }; + + private static final 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 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 TaskFilter CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER = + new TaskFilter() { + @Override + public boolean shouldAccept(Runnable command) { + return command.toString() + .contains(XdsClientImpl.CdsResourceFetchTimeoutTask.class.getSimpleName()); + } + }; + + private static final TaskFilter EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER = + new TaskFilter() { + @Override + public boolean shouldAccept(Runnable command) { + return command.toString() + .contains(XdsClientImpl.EdsResourceFetchTimeoutTask.class.getSimpleName()); + } + }; + + @Rule + public final GrpcCleanupRule cleanupRule = new GrpcCleanupRule(); + @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 ClusterWatcher clusterWatcher; + @Mock + private EndpointWatcher endpointWatcher; + + 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()); + + List servers = + ImmutableList.of(new ServerInfo(serverName, ImmutableList.of(), null)); + XdsChannelFactory channelFactory = new XdsChannelFactory() { + @Override + XdsChannel createChannel(List servers) { + ServerInfo serverInfo = Iterables.getOnlyElement(servers); + assertThat(serverInfo.getServerUri()).isEqualTo(serverName); + assertThat(serverInfo.getChannelCredentials()).isEmpty(); + return new XdsChannel(channel, false); + } + }; + + xdsClient = + new XdsClientImpl( + TARGET_AUTHORITY, + servers, + channelFactory, + 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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + + 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 = + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sends an ACK LDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, "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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); + + io.envoyproxy.envoy.api.v2.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 = + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sends an NACK LDS request. + verify(requestObserver) + .onNext( + argThat(new DiscoveryRequestMatcher("", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, "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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + 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 = + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + responseObserver.onNext(response); + + assertThat(ldsRespTimer.isCancelled()).isTrue(); + + // Client sends an ACK request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, "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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + + 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 = + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sends an ACK LDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + + // Client sends an (first) RDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "route-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_RDS_V2, ""))); + + 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 = buildDiscoveryResponseV2( + "0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sends an ACK RDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", "route-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_RDS_V2, "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 = + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "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 = buildDiscoveryResponseV2( + "0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0000"); + responseObserver.onNext(response); + + assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); + + // Client sent an ACK RDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", "route-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_RDS_V2, "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 = + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "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( + io.envoyproxy.envoy.api.v2.route.VirtualHost.newBuilder() + .setName("virtualhost00.googleapis.com") // don't care + // domains wit a match. + .addAllDomains(ImmutableList.of(TARGET_AUTHORITY, "bar.googleapis.com")) + .addRoutes( + io.envoyproxy.envoy.api.v2.route.Route.newBuilder() + // path match with cluster route + .setRoute( + io.envoyproxy.envoy.api.v2.route.RouteAction.newBuilder() + .setCluster("cl1.googleapis.com")) + .setMatch( + io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder() + .setPath("/service1/method1"))) + .addRoutes( + io.envoyproxy.envoy.api.v2.route.Route.newBuilder() + // path match with weighted cluster route + .setRoute( + io.envoyproxy.envoy.api.v2.route.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( + io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder() + .setPath("/service2/method2"))) + .addRoutes( + io.envoyproxy.envoy.api.v2.route.Route.newBuilder() + // prefix match with cluster route + .setRoute( + io.envoyproxy.envoy.api.v2.route.RouteAction.newBuilder() + .setCluster("cl1.googleapis.com")) + .setMatch( + io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder() + .setPrefix("/service1/"))) + .addRoutes( + io.envoyproxy.envoy.api.v2.route.Route.newBuilder() + // default match with cluster route + .setRoute( + io.envoyproxy.envoy.api.v2.route.RouteAction.newBuilder() + .setCluster("cluster.googleapis.com")) + .setMatch( + io.envoyproxy.envoy.api.v2.route.RouteMatch.newBuilder() + .setPrefix(""))) + .build())))); + response = buildDiscoveryResponseV2( + "0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0000"); + responseObserver.onNext(response); + + assertThat(fakeClock.getPendingTasks(RDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); + + // Client sent an ACK RDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", "route-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_RDS_V2, "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( + /* prefix= */ null, + /* path= */ "/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( + /* prefix= */ null, + /* path= */ "/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( + /* prefix= */ "/service1/", + /* path= */ 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( + /* prefix= */ "", + /* path= */ 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 = + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "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 = buildDiscoveryResponseV2( + "0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, "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_V2, "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 = + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "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. + io.envoyproxy.envoy.api.v2.route.VirtualHost virtualHost = + io.envoyproxy.envoy.api.v2.route.VirtualHost.newBuilder() + .setName("virtualhost00.googleapis.com") // don't care + .addDomains(TARGET_AUTHORITY) + .addRoutes( + io.envoyproxy.envoy.api.v2.route.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 = buildDiscoveryResponseV2( + "0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, "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_V2, "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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + + // Management server sends back an LDS response containing a RouteConfiguration for the + // requested Listener directly in-line. + io.envoyproxy.envoy.api.v2.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 = + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sends an ACK LDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, "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 = + buildDiscoveryResponseV2("1", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0001"); + responseObserver.onNext(response); + + // Client sends an ACK LDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "1", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, "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 = + buildDiscoveryResponseV2("2", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0002"); + responseObserver.onNext(response); + + // Client sends an ACK LDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "2", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0002"))); + + // Client sends an (first) RDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "some-route-to-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_RDS_V2, ""))); + + // 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 = buildDiscoveryResponseV2( + "0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sent an ACK RDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", "some-route-to-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_RDS_V2, "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 = buildDiscoveryResponseV2( + "1", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0001"); + responseObserver.onNext(response); + + // Client sent an ACK RDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "1", "some-route-to-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_RDS_V2, "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 = + buildDiscoveryResponseV2("3", ImmutableList.of(), + XdsClientImpl.ADS_TYPE_URL_LDS_V2, "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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + + // 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 = + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sends an ACK LDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + + // Client sends an (first) RDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "route-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_RDS_V2, ""))); + + 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 = buildDiscoveryResponseV2( + "0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sent an ACK RDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", "route-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_RDS_V2, "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 = buildDiscoveryResponseV2( + "1", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0001"); + responseObserver.onNext(response); + + // Client sent an ACK RDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "1", "route-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_RDS_V2, "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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + + // 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 = + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sends an ACK LDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + + // Client sends an (first) RDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "route-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_RDS_V2, ""))); + + // 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 = buildDiscoveryResponseV2( + "0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sent an ACK RDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", "route-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_RDS_V2, "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 = + buildDiscoveryResponseV2("1", ImmutableList.of(), // empty + XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0001"); + responseObserver.onNext(response); + + // Client sent an ACK LDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "1", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, "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 = + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sends an (first) RDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "route-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_RDS_V2, ""))); + + 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 = buildDiscoveryResponseV2("1", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0001"); + responseObserver.onNext(response); + + // Client sent a new RDS request with updated resource name. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "route-bar.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_RDS_V2, ""))); + + 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 = buildDiscoveryResponseV2( + "0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, "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.watchClusterData("cluster-foo.googleapis.com", clusterWatcher); + 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(buildDiscoveryRequestV2(NODE, "", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + 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 = + buildDiscoveryResponseV2("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sent an ACK CDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"))); + verify(clusterWatcher, never()).onClusterChanged(any(ClusterUpdate.class)); + verify(clusterWatcher, never()).onResourceDoesNotExist("cluster-foo.googleapis.com"); + verify(clusterWatcher, never()).onError(any(Status.class)); + + fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); + verify(clusterWatcher).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.watchClusterData("cluster-foo.googleapis.com", clusterWatcher); + 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(buildDiscoveryRequestV2(NODE, "", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + 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 = + buildDiscoveryResponseV2("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sent an ACK CDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"))); + assertThat(cdsRespTimer.isCancelled()).isTrue(); + + ArgumentCaptor clusterUpdateCaptor = ArgumentCaptor.forClass(null); + verify(clusterWatcher).onClusterChanged(clusterUpdateCaptor.capture()); + ClusterUpdate clusterUpdate = clusterUpdateCaptor.getValue(); + assertThat(clusterUpdate.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(clusterUpdate.getEdsServiceName()).isNull(); + assertThat(clusterUpdate.getLbPolicy()).isEqualTo("round_robin"); + assertThat(clusterUpdate.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 = + buildDiscoveryResponseV2("1", clusters, XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0001"); + responseObserver.onNext(response); + + // Client sent an ACK CDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "1", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0001"))); + + verify(clusterWatcher, times(2)).onClusterChanged(clusterUpdateCaptor.capture()); + clusterUpdate = clusterUpdateCaptor.getValue(); + assertThat(clusterUpdate.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(clusterUpdate.getEdsServiceName()) + .isEqualTo("eds-cluster-foo.googleapis.com"); + assertThat(clusterUpdate.getLbPolicy()).isEqualTo("round_robin"); + assertThat(clusterUpdate.getLrsServerName()).isEqualTo(""); + } + + /** + * CDS response containing UpstreamTlsContext for a cluster. + */ + @Test + public void cdsResponseWithUpstreamTlsContext() { + xdsClient.watchClusterData("cluster-foo.googleapis.com", clusterWatcher); + 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 = + buildDiscoveryResponseV2("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sent an ACK CDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"))); + ArgumentCaptor clusterUpdateCaptor = ArgumentCaptor.forClass(null); + verify(clusterWatcher, times(1)).onClusterChanged(clusterUpdateCaptor.capture()); + ClusterUpdate clusterUpdate = clusterUpdateCaptor.getValue(); + EnvoyServerProtoData.UpstreamTlsContext upstreamTlsContext = clusterUpdate + .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 multipleClusterWatchers() { + ClusterWatcher watcher1 = mock(ClusterWatcher.class); + ClusterWatcher watcher2 = mock(ClusterWatcher.class); + ClusterWatcher watcher3 = mock(ClusterWatcher.class); + xdsClient.watchClusterData("cluster-foo.googleapis.com", watcher1); + xdsClient.watchClusterData("cluster-foo.googleapis.com", watcher2); + xdsClient.watchClusterData("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_V2, ""))); + 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 = + buildDiscoveryResponseV2("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS_V2, "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_V2, "0000"))); + + // Two watchers get notification of cluster update for the cluster they are interested in. + ArgumentCaptor clusterUpdateCaptor1 = ArgumentCaptor.forClass(null); + verify(watcher1).onClusterChanged(clusterUpdateCaptor1.capture()); + ClusterUpdate clusterUpdate1 = clusterUpdateCaptor1.getValue(); + assertThat(clusterUpdate1.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(clusterUpdate1.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(clusterUpdate1.getEdsServiceName()).isNull(); + assertThat(clusterUpdate1.getLbPolicy()).isEqualTo("round_robin"); + assertThat(clusterUpdate1.getLrsServerName()).isNull(); + + ArgumentCaptor clusterUpdateCaptor2 = ArgumentCaptor.forClass(null); + verify(watcher2).onClusterChanged(clusterUpdateCaptor2.capture()); + ClusterUpdate clusterUpdate2 = clusterUpdateCaptor2.getValue(); + assertThat(clusterUpdate2.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(clusterUpdate2.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(clusterUpdate2.getEdsServiceName()).isNull(); + assertThat(clusterUpdate2.getLbPolicy()).isEqualTo("round_robin"); + assertThat(clusterUpdate2.getLrsServerName()).isNull(); + + verify(watcher3, never()).onClusterChanged(any(ClusterUpdate.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 = buildDiscoveryResponseV2("1", clusters, + XdsClientImpl.ADS_TYPE_URL_CDS_V2, "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_V2, "0001"))); + + verifyNoMoreInteractions(watcher1, watcher2); // resource has no change + ArgumentCaptor clusterUpdateCaptor3 = ArgumentCaptor.forClass(null); + verify(watcher3).onClusterChanged(clusterUpdateCaptor3.capture()); + ClusterUpdate clusterUpdate3 = clusterUpdateCaptor3.getValue(); + assertThat(clusterUpdate3.getClusterName()).isEqualTo("cluster-bar.googleapis.com"); + assertThat(clusterUpdate3.getEdsServiceName()) + .isEqualTo("eds-cluster-bar.googleapis.com"); + assertThat(clusterUpdate3.getLbPolicy()).isEqualTo("round_robin"); + assertThat(clusterUpdate3.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() { + ClusterWatcher watcher1 = mock(ClusterWatcher.class); + xdsClient.watchClusterData("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(buildDiscoveryRequestV2(NODE, "", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + 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 = + buildDiscoveryResponseV2("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sent an ACK CDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"))); + + ArgumentCaptor clusterUpdateCaptor1 = ArgumentCaptor.forClass(null); + verify(watcher1).onClusterChanged(clusterUpdateCaptor1.capture()); + ClusterUpdate clusterUpdate1 = clusterUpdateCaptor1.getValue(); + assertThat(clusterUpdate1.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(clusterUpdate1.getEdsServiceName()).isNull(); + assertThat(clusterUpdate1.getLbPolicy()).isEqualTo("round_robin"); + assertThat(clusterUpdate1.getLrsServerName()).isNull(); + assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); + + // Another cluster watcher interested in the same cluster is added. + ClusterWatcher watcher2 = mock(ClusterWatcher.class); + xdsClient.watchClusterData("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 clusterUpdateCaptor2 = ArgumentCaptor.forClass(null); + verify(watcher2).onClusterChanged(clusterUpdateCaptor2.capture()); + ClusterUpdate clusterUpdate2 = clusterUpdateCaptor2.getValue(); + assertThat(clusterUpdate2.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(clusterUpdate2.getEdsServiceName()).isNull(); + assertThat(clusterUpdate2.getLbPolicy()).isEqualTo("round_robin"); + assertThat(clusterUpdate2.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 addRemoveClusterWatchers() { + ClusterWatcher watcher1 = mock(ClusterWatcher.class); + xdsClient.watchClusterData("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(buildDiscoveryRequestV2(NODE, "", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + + // 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 = + buildDiscoveryResponseV2("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sent an ACK CDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"))); + + ArgumentCaptor clusterUpdateCaptor1 = ArgumentCaptor.forClass(null); + verify(watcher1).onClusterChanged(clusterUpdateCaptor1.capture()); + ClusterUpdate clusterUpdate1 = clusterUpdateCaptor1.getValue(); + assertThat(clusterUpdate1.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(clusterUpdate1.getEdsServiceName()).isNull(); + assertThat(clusterUpdate1.getLbPolicy()).isEqualTo("round_robin"); + assertThat(clusterUpdate1.getLrsServerName()).isNull(); + + // Add another cluster watcher for a different cluster. + ClusterWatcher watcher2 = mock(ClusterWatcher.class); + xdsClient.watchClusterData("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_V2, "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 = buildDiscoveryResponseV2("1", clusters, + XdsClientImpl.ADS_TYPE_URL_CDS_V2, "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_V2, "0001"))); + verifyNoMoreInteractions(watcher1); // resource has no change + ArgumentCaptor clusterUpdateCaptor2 = ArgumentCaptor.forClass(null); + verify(watcher2).onClusterChanged(clusterUpdateCaptor2.capture()); + ClusterUpdate clusterUpdate2 = clusterUpdateCaptor2.getValue(); + assertThat(clusterUpdate2.getClusterName()).isEqualTo("cluster-bar.googleapis.com"); + assertThat(clusterUpdate2.getEdsServiceName()) + .isEqualTo("eds-cluster-bar.googleapis.com"); + assertThat(clusterUpdate2.getLbPolicy()).isEqualTo("round_robin"); + assertThat(clusterUpdate2.getLrsServerName()).isEqualTo(""); + + // Cancel one of the watcher. + xdsClient.cancelClusterDataWatch("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(buildDiscoveryRequestV2(NODE, "1", "cluster-bar.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0001"))); + + // Management server has nothing to respond. + + // Cancel the other watcher. All resources have been unsubscribed. + xdsClient.cancelClusterDataWatch("cluster-bar.googleapis.com", watcher2); + + verify(requestObserver) + .onNext( + argThat( + new DiscoveryRequestMatcher("1", ImmutableList.of(), + XdsClientImpl.ADS_TYPE_URL_CDS_V2, "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 = + buildDiscoveryResponseV2("2", clusters, XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0002"); + responseObserver.onNext(response); + + verify(requestObserver) + .onNext( + argThat( + new DiscoveryRequestMatcher("2", ImmutableList.of(), + XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0002"))); + + // Cancelled watchers do not receive notification. + verifyNoMoreInteractions(watcher1, watcher2); + + // A new cluster watcher is added to watch cluster foo again. + ClusterWatcher watcher3 = mock(ClusterWatcher.class); + xdsClient.watchClusterData("cluster-foo.googleapis.com", watcher3); + verify(watcher3, never()).onClusterChanged(any(ClusterUpdate.class)); + + // A CDS request is sent to indicate subscription of "cluster-foo.googleapis.com" only. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "2", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, "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 = + buildDiscoveryResponseV2("3", clusters, XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0003"); + responseObserver.onNext(response); + + // Notified with cached data immediately. + ArgumentCaptor clusterUpdateCaptor3 = ArgumentCaptor.forClass(null); + verify(watcher3).onClusterChanged(clusterUpdateCaptor3.capture()); + ClusterUpdate clusterUpdate3 = clusterUpdateCaptor3.getValue(); + assertThat(clusterUpdate3.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(clusterUpdate3.getEdsServiceName()).isNull(); + assertThat(clusterUpdate3.getLbPolicy()).isEqualTo("round_robin"); + assertThat(clusterUpdate2.getLrsServerName()).isEqualTo(""); + + verifyNoMoreInteractions(watcher1, watcher2); + + // A CDS request is sent to re-subscribe the cluster again. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "3", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0003"))); + } + + @Test + public void addRemoveClusterWatcherWhileInitialResourceFetchInProgress() { + ClusterWatcher watcher1 = mock(ClusterWatcher.class); + xdsClient.watchClusterData("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_CDS_V2, ""))); + assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); + + fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC - 1, TimeUnit.SECONDS); + + ClusterWatcher watcher2 = mock(ClusterWatcher.class); + ClusterWatcher watcher3 = mock(ClusterWatcher.class); + ClusterWatcher watcher4 = mock(ClusterWatcher.class); + xdsClient.watchClusterData("cluster-foo.googleapis.com", watcher2); + xdsClient.watchClusterData("cluster-bar.googleapis.com", watcher3); + xdsClient.watchClusterData("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_V2, ""))); + 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. + ClusterWatcher watcher5 = mock(ClusterWatcher.class); + xdsClient.watchClusterData("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.cancelClusterDataWatch("cluster-bar.googleapis.com", watcher3); + assertThat(timeoutTask.isCancelled()).isFalse(); + xdsClient.cancelClusterDataWatch("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(); + + // TODO(chengyuanzhang): migrate to verifyNoInteractions. + verifyNoMoreInteractions(watcher3, watcher4); + } + + @Test + public void cdsUpdateForClusterBeingRemoved() { + xdsClient.watchClusterData("cluster-foo.googleapis.com", clusterWatcher); + StreamObserver responseObserver = responseObservers.poll(); + StreamObserver requestObserver = requestObservers.poll(); + + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + 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 = + buildDiscoveryResponseV2("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sent an ACK CDS request (Omitted). + + ArgumentCaptor clusterUpdateCaptor = ArgumentCaptor.forClass(null); + verify(clusterWatcher).onClusterChanged(clusterUpdateCaptor.capture()); + ClusterUpdate clusterUpdate = clusterUpdateCaptor.getValue(); + assertThat(clusterUpdate.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(clusterUpdate.getEdsServiceName()).isNull(); + assertThat(clusterUpdate.getLbPolicy()).isEqualTo("round_robin"); + assertThat(clusterUpdate.getLrsServerName()).isEqualTo(""); + assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); + + // No cluster is available. + response = + buildDiscoveryResponseV2("1", ImmutableList.of(), + XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0001"); + responseObserver.onNext(response); + + verify(clusterWatcher).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.watchEndpointData("cluster-foo.googleapis.com", endpointWatcher); + 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(buildDiscoveryRequestV2(NODE, "", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + 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 = + buildDiscoveryResponseV2("0", clusterLoadAssignments, + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sent an ACK EDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0000"))); + + verify(endpointWatcher, never()).onEndpointChanged(any(EndpointUpdate.class)); + verify(endpointWatcher, never()).onResourceDoesNotExist("cluster-foo.googleapis.com"); + verify(endpointWatcher, never()).onError(any(Status.class)); + fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); + verify(endpointWatcher).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.watchEndpointData("cluster-foo.googleapis.com", endpointWatcher); + 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(buildDiscoveryRequestV2(NODE, "", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + 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 = + buildDiscoveryResponseV2("0", clusterLoadAssignments, + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0000"); + responseObserver.onNext(response); + + assertThat(edsRespTimeoutTask.isCancelled()).isTrue(); + + // Client sent an ACK EDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0000"))); + + ArgumentCaptor endpointUpdateCaptor = ArgumentCaptor.forClass(null); + verify(endpointWatcher).onEndpointChanged(endpointUpdateCaptor.capture()); + EndpointUpdate endpointUpdate = endpointUpdateCaptor.getValue(); + assertThat(endpointUpdate.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(endpointUpdate.getDropPolicies()) + .containsExactly( + new DropOverload("lb", 200), + new DropOverload("throttle", 1000)); + assertThat(endpointUpdate.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 = + buildDiscoveryResponseV2( + "1", clusterLoadAssignments, XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0001"); + responseObserver.onNext(response); + + // Client sent an ACK EDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "1", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0001"))); + + verify(endpointWatcher, times(2)).onEndpointChanged(endpointUpdateCaptor.capture()); + endpointUpdate = endpointUpdateCaptor.getValue(); + assertThat(endpointUpdate.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(endpointUpdate.getDropPolicies()).isEmpty(); + assertThat(endpointUpdate.getLocalityLbEndpointsMap()).isEmpty(); + } + + @Test + public void multipleEndpointWatchers() { + EndpointWatcher watcher1 = mock(EndpointWatcher.class); + EndpointWatcher watcher2 = mock(EndpointWatcher.class); + EndpointWatcher watcher3 = mock(EndpointWatcher.class); + xdsClient.watchEndpointData("cluster-foo.googleapis.com", watcher1); + xdsClient.watchEndpointData("cluster-foo.googleapis.com", watcher2); + xdsClient.watchEndpointData("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_V2, ""))); + + 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 = + buildDiscoveryResponseV2("0", clusterLoadAssignments, + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "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_V2, "0000"))); + + // Two watchers get notification of endpoint update for the cluster they are interested in. + ArgumentCaptor endpointUpdateCaptor1 = ArgumentCaptor.forClass(null); + verify(watcher1).onEndpointChanged(endpointUpdateCaptor1.capture()); + EndpointUpdate endpointUpdate1 = endpointUpdateCaptor1.getValue(); + assertThat(endpointUpdate1.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(endpointUpdate1.getLocalityLbEndpointsMap()) + .containsExactly( + new Locality("region1", "zone1", "subzone1"), + new LocalityLbEndpoints( + ImmutableList.of( + new LbEndpoint("192.168.0.1", 8080, + 2, true)), 1, 0)); + + ArgumentCaptor endpointUpdateCaptor2 = ArgumentCaptor.forClass(null); + verify(watcher1).onEndpointChanged(endpointUpdateCaptor2.capture()); + EndpointUpdate endpointUpdate2 = endpointUpdateCaptor2.getValue(); + assertThat(endpointUpdate2.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(endpointUpdate2.getLocalityLbEndpointsMap()) + .containsExactly( + new Locality("region1", "zone1", "subzone1"), + new LocalityLbEndpoints( + ImmutableList.of( + new LbEndpoint("192.168.0.1", 8080, + 2, true)), 1, 0)); + + // TODO(chengyuanzhang): migrate to verifyNoInteractions. + verifyNoMoreInteractions(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 = buildDiscoveryResponseV2("1", clusterLoadAssignments, + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "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_V2, "0001"))); + + // The corresponding watcher gets notified. + ArgumentCaptor endpointUpdateCaptor3 = ArgumentCaptor.forClass(null); + verify(watcher3).onEndpointChanged(endpointUpdateCaptor3.capture()); + EndpointUpdate endpointUpdate3 = endpointUpdateCaptor3.getValue(); + assertThat(endpointUpdate3.getClusterName()).isEqualTo("cluster-bar.googleapis.com"); + assertThat(endpointUpdate3.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() { + EndpointWatcher watcher1 = mock(EndpointWatcher.class); + xdsClient.watchEndpointData("cluster-foo.googleapis.com", watcher1); + StreamObserver responseObserver = responseObservers.poll(); + StreamObserver requestObserver = requestObservers.poll(); + + // Client sends first EDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + + 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 = + buildDiscoveryResponseV2("0", clusterLoadAssignments, + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0000"); + responseObserver.onNext(response); + + assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); + + // Client sent an ACK EDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0000"))); + + ArgumentCaptor endpointUpdateCaptor1 = ArgumentCaptor.forClass(null); + verify(watcher1).onEndpointChanged(endpointUpdateCaptor1.capture()); + EndpointUpdate endpointUpdate1 = endpointUpdateCaptor1.getValue(); + assertThat(endpointUpdate1.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(endpointUpdate1.getDropPolicies()).isEmpty(); + assertThat(endpointUpdate1.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. + EndpointWatcher watcher2 = mock(EndpointWatcher.class); + xdsClient.watchEndpointData("cluster-foo.googleapis.com", watcher2); + + // Cached endpoint information is notified to the new watcher immediately, without sending + // another EDS request. + ArgumentCaptor endpointUpdateCaptor2 = ArgumentCaptor.forClass(null); + verify(watcher2).onEndpointChanged(endpointUpdateCaptor2.capture()); + EndpointUpdate endpointUpdate2 = endpointUpdateCaptor2.getValue(); + assertThat(endpointUpdate2.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(endpointUpdate2.getDropPolicies()).isEmpty(); + assertThat(endpointUpdate2.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 addRemoveEndpointWatchers() { + EndpointWatcher watcher1 = mock(EndpointWatcher.class); + xdsClient.watchEndpointData("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(buildDiscoveryRequestV2(NODE, "", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + + // 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 = + buildDiscoveryResponseV2("0", clusterLoadAssignments, + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0000"); + responseObserver.onNext(response); + + // Client sent an ACK EDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", "cluster-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0000"))); + + ArgumentCaptor endpointUpdateCaptor1 = ArgumentCaptor.forClass(null); + verify(watcher1).onEndpointChanged(endpointUpdateCaptor1.capture()); + EndpointUpdate endpointUpdate1 = endpointUpdateCaptor1.getValue(); + assertThat(endpointUpdate1.getClusterName()).isEqualTo("cluster-foo.googleapis.com"); + assertThat(endpointUpdate1.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. + EndpointWatcher watcher2 = mock(EndpointWatcher.class); + xdsClient.watchEndpointData("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_V2, "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 = buildDiscoveryResponseV2("1", clusterLoadAssignments, + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "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_V2, "0001"))); + + ArgumentCaptor endpointUpdateCaptor2 = ArgumentCaptor.forClass(null); + verify(watcher2).onEndpointChanged(endpointUpdateCaptor2.capture()); + EndpointUpdate endpointUpdate2 = endpointUpdateCaptor2.getValue(); + assertThat(endpointUpdate2.getClusterName()).isEqualTo("cluster-bar.googleapis.com"); + assertThat(endpointUpdate2.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.cancelEndpointDataWatch("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(buildDiscoveryRequestV2(NODE, "1", "cluster-bar.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0001"))); + + // Management server should not respond as it had previously sent the requested resource. + + // Cancel the other watcher. + xdsClient.cancelEndpointDataWatch("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_V2, "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 = buildDiscoveryResponseV2("2", clusterLoadAssignments, + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "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_V2, "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. + EndpointWatcher watcher3 = mock(EndpointWatcher.class); + xdsClient.watchEndpointData("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()).onEndpointChanged(any(EndpointUpdate.class)); + + // An EDS request is sent to re-subscribe the cluster again. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "2", "cluster-bar.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "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 = buildDiscoveryResponseV2("3", clusterLoadAssignments, + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0003"); + responseObserver.onNext(response); + + ArgumentCaptor endpointUpdateCaptor3 = ArgumentCaptor.forClass(null); + verify(watcher3).onEndpointChanged(endpointUpdateCaptor3.capture()); + EndpointUpdate endpointUpdate3 = endpointUpdateCaptor3.getValue(); + assertThat(endpointUpdate3.getClusterName()).isEqualTo("cluster-bar.googleapis.com"); + assertThat(endpointUpdate3.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_V2, "0003"))); + } + + @Test + public void addRemoveEndpointWatcherWhileInitialResourceFetchInProgress() { + EndpointWatcher watcher1 = mock(EndpointWatcher.class); + xdsClient.watchEndpointData("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_V2, ""))); + assertThat(fakeClock.getPendingTasks(EDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).hasSize(1); + + fakeClock.forwardTime(XdsClientImpl.INITIAL_RESOURCE_FETCH_TIMEOUT_SEC - 1, TimeUnit.SECONDS); + + EndpointWatcher watcher2 = mock(EndpointWatcher.class); + EndpointWatcher watcher3 = mock(EndpointWatcher.class); + EndpointWatcher watcher4 = mock(EndpointWatcher.class); + xdsClient.watchEndpointData("cluster-foo.googleapis.com", watcher2); + xdsClient.watchEndpointData("cluster-bar.googleapis.com", watcher3); + xdsClient.watchEndpointData("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_V2, ""))); + 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. + EndpointWatcher watcher5 = mock(EndpointWatcher.class); + xdsClient.watchEndpointData("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.cancelEndpointDataWatch("cluster-bar.googleapis.com", watcher3); + assertThat(timeoutTask.isCancelled()).isFalse(); + xdsClient.cancelEndpointDataWatch("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(); + + // TODO(chengyuanzhang): migrate to verifyNoInteractions. + verifyNoMoreInteractions(watcher3, watcher4); + } + + @Test + public void cdsUpdateForEdsServiceNameChange() { + xdsClient.watchClusterData("cluster-foo.googleapis.com", clusterWatcher); + 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 = + buildDiscoveryResponseV2("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"); + responseObserver.onNext(response); + + xdsClient.watchEndpointData("cluster-foo:service-bar", endpointWatcher); + + // 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 = + buildDiscoveryResponseV2("0", clusterLoadAssignments, + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "0000"); + responseObserver.onNext(response); + + ArgumentCaptor endpointUpdateCaptor = ArgumentCaptor.forClass(null); + verify(endpointWatcher).onEndpointChanged(endpointUpdateCaptor.capture()); + EndpointUpdate endpointUpdate = endpointUpdateCaptor.getValue(); + assertThat(endpointUpdate.getClusterName()).isEqualTo("cluster-foo:service-bar"); + assertThat(endpointUpdate.getDropPolicies()).isEmpty(); + assertThat(endpointUpdate.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 = + buildDiscoveryResponseV2("1", clusters, XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0001"); + responseObserver.onNext(response); + + // Watcher get notification for endpoint resource "cluster-foo:service-bar" being deleted. + verify(endpointWatcher).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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + + // 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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + + // 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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + + // 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 = + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + responseObserver.onNext(ldsResponse); + + // Client sent back an ACK LDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "0", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"))); + + // Client sent an RDS request based on the received listener. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "route-foo.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_RDS_V2, ""))); + + // 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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + + // 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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + + // Management server sends an LDS response. + 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 = + buildDiscoveryResponseV2("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, "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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + + 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.watchClusterData("cluster.googleapis.com", clusterWatcher); + + // Client sent first CDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + + // Start watching endpoint information. + xdsClient.watchEndpointData("cluster.googleapis.com", endpointWatcher); + + // Client sent first EDS request. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + + // 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(clusterWatcher).onError(statusCaptor.capture()); + assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNKNOWN); + verify(endpointWatcher).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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + + // Management server becomes unreachable. + responseObserver.onError(Status.UNAVAILABLE.asException()); + verify(configWatcher, times(2)).onError(statusCaptor.capture()); + assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNAVAILABLE); + verify(clusterWatcher, times(2)).onError(statusCaptor.capture()); + assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNAVAILABLE); + verify(endpointWatcher, 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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + + // 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(clusterWatcher, times(3)).onError(statusCaptor.capture()); + assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNAVAILABLE); + verify(endpointWatcher, 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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + + // Management server sends back a CDS response. + List clusters = ImmutableList.of( + Any.pack(buildCluster("cluster.googleapis.com", null, false))); + DiscoveryResponse cdsResponse = + buildDiscoveryResponseV2("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS_V2, "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(clusterWatcher, times(4)).onError(any(Status.class)); + verify(endpointWatcher, 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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + + // 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(clusterWatcher, times(5)).onError(statusCaptor.capture()); + assertThat(statusCaptor.getValue().getCode()).isEqualTo(Code.UNAVAILABLE); + verify(endpointWatcher, 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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + + 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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + + // 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.watchClusterData("cluster.googleapis.com", clusterWatcher); + + // 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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + + // 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.watchEndpointData("cluster.googleapis.com", endpointWatcher); + + // 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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + + // Management server sends back a CDS response. + List clusters = ImmutableList.of( + Any.pack(buildCluster("cluster.googleapis.com", null, false))); + DiscoveryResponse cdsResponse = + buildDiscoveryResponseV2("0", clusters, XdsClientImpl.ADS_TYPE_URL_CDS_V2, "0000"); + responseObserver.onNext(cdsResponse); + + // Client sent an CDS ACK request (Omitted). + + // No longer interested in endpoint information after RPC resumes. + xdsClient.cancelEndpointDataWatch("cluster.googleapis.com", endpointWatcher); + // Client updates EDS resource subscription immediately. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", ImmutableList.of(), + XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + + // Become interested in endpoints of another cluster. + xdsClient.watchEndpointData("cluster2.googleapis.com", endpointWatcher); + // Client updates EDS resource subscription immediately. + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster2.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + + // 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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + verify(requestObserver) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster2.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + + // 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.cancelClusterDataWatch("cluster.googleapis.com", clusterWatcher); + xdsClient.cancelEndpointDataWatch("cluster2.googleapis.com", endpointWatcher); + + // 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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + verify(requestObserver, never()) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_CDS_V2, ""))); + verify(requestObserver, never()) + .onNext(eq(buildDiscoveryRequestV2(NODE, "", "cluster2.googleapis.com", + XdsClientImpl.ADS_TYPE_URL_EDS_V2, ""))); + + 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 = + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "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(buildDiscoveryRequestV2(NODE, "", TARGET_AUTHORITY, + XdsClientImpl.ADS_TYPE_URL_LDS_V2, ""))); + 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 = buildDiscoveryResponseV2( + "0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, "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.watchClusterData("cluster-foo.googleapis.com", clusterWatcher); + 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.watchEndpointData("cluster-foo.googleapis.com", endpointWatcher); + 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"; + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(null); + xdsClient.addClientStats(clusterName, 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 = + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "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 = + buildDiscoveryResponseV2("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, "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 = + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000"); + + String expectedString = "{\n" + + " \"versionInfo\": \"0\",\n" + + " \"resources\": [{\n" + + " \"@type\": \"type.googleapis.com/envoy.api.v2.Listener\",\n" + + " \"name\": \"foo.googleapis.com:8080\",\n" + + " \"address\": {\n" + + " },\n" + + " \"filterChains\": [{\n" + + " }],\n" + + " \"apiListener\": {\n" + + " \"apiListener\": {\n" + + " \"@type\": \"type.googleapis.com/envoy.config.filter.network" + + ".http_connection_manager.v2.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.api.v2.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 = + buildDiscoveryResponseV2("213", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0052"); + + String expectedString = "{\n" + + " \"versionInfo\": \"213\",\n" + + " \"resources\": [{\n" + + " \"@type\": \"type.googleapis.com/envoy.api.v2.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.api.v2.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 = + buildDiscoveryResponseV2("14", clusters, XdsClientImpl.ADS_TYPE_URL_CDS_V2, "8"); + + String expectedString = "{\n" + + " \"versionInfo\": \"14\",\n" + + " \"resources\": [{\n" + + " \"@type\": \"type.googleapis.com/envoy.api.v2.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.api.v2.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.api.v2.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 = + buildDiscoveryResponseV2("5", clusterLoadAssignments, + XdsClientImpl.ADS_TYPE_URL_EDS_V2, "004"); + + String expectedString = "{\n" + + " \"versionInfo\": \"5\",\n" + + " \"resources\": [{\n" + + " \"@type\": \"type.googleapis.com/envoy.api.v2.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" + + " \"disableOverprovisioning\": true\n" + + " }\n" + + " }],\n" + + " \"typeUrl\": \"type.googleapis.com/envoy.api.v2.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.toEnvoyProtoNodeV2()); + } + } + + 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/XdsClientTest.java b/xds/src/test/java/io/grpc/xds/XdsClientTest.java index 56fca6dc09b..9278a80dbb5 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientTest.java @@ -21,7 +21,12 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import com.google.common.collect.ImmutableList; +import io.grpc.xds.Bootstrapper.ChannelCreds; +import io.grpc.xds.Bootstrapper.ServerInfo; import io.grpc.xds.XdsClient.RefCountedXdsClientObjectPool; +import io.grpc.xds.XdsClient.XdsChannel; +import io.grpc.xds.XdsClient.XdsChannelFactory; import io.grpc.xds.XdsClient.XdsClientFactory; import org.junit.Rule; import org.junit.Test; @@ -102,4 +107,58 @@ XdsClient createXdsClient() { XdsClient xdsClient2 = xdsClientPool.getObject(); assertThat(xdsClient2).isNotSameInstanceAs(xdsClient1); } + + @Test + public void channelFactorySupportsV3() { + boolean originalV3SupportEnvVar = XdsChannelFactory.experimentalV3SupportEnvVar; + try { + XdsChannelFactory xdsChannelFactory = XdsChannelFactory.getInstance(); + XdsChannelFactory.experimentalV3SupportEnvVar = true; + XdsChannel xdsChannel = + xdsChannelFactory.createChannel( + ImmutableList.of( + new ServerInfo( + "xdsserver.com", + ImmutableList.of(), + ImmutableList.of()), + new ServerInfo( + "xdsserver2.com", + ImmutableList.of(), + ImmutableList.of("xds_v3")))); + xdsChannel.getManagedChannel().shutdown(); + assertThat(xdsChannel.isUseProtocolV3()).isFalse(); + + XdsChannelFactory.experimentalV3SupportEnvVar = false; + xdsChannel = + xdsChannelFactory.createChannel( + ImmutableList.of( + new ServerInfo( + "xdsserver.com", + ImmutableList.of(), + ImmutableList.of("xds_v3")), + new ServerInfo( + "xdsserver2.com", + ImmutableList.of(), + ImmutableList.of("baz")))); + xdsChannel.getManagedChannel().shutdown(); + assertThat(xdsChannel.isUseProtocolV3()).isFalse(); + + XdsChannelFactory.experimentalV3SupportEnvVar = true; + xdsChannel = + xdsChannelFactory.createChannel( + ImmutableList.of( + new ServerInfo( + "xdsserver.com", + ImmutableList.of(), + ImmutableList.of("xds_v3")), + new ServerInfo( + "xdsserver2.com", + ImmutableList.of(), + ImmutableList.of("baz")))); + xdsChannel.getManagedChannel().shutdown(); + assertThat(xdsChannel.isUseProtocolV3()).isTrue(); + } finally { + XdsChannelFactory.experimentalV3SupportEnvVar = originalV3SupportEnvVar; + } + } } diff --git a/xds/src/test/java/io/grpc/xds/XdsClientTestHelper.java b/xds/src/test/java/io/grpc/xds/XdsClientTestHelper.java index 868fb426379..ab976e877e1 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientTestHelper.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientTestHelper.java @@ -25,8 +25,6 @@ import io.envoyproxy.envoy.api.v2.Cluster.LbPolicy; import io.envoyproxy.envoy.api.v2.ClusterLoadAssignment; import io.envoyproxy.envoy.api.v2.ClusterLoadAssignment.Policy; -import io.envoyproxy.envoy.api.v2.DiscoveryRequest; -import io.envoyproxy.envoy.api.v2.DiscoveryResponse; import io.envoyproxy.envoy.api.v2.Listener; import io.envoyproxy.envoy.api.v2.RouteConfiguration; import io.envoyproxy.envoy.api.v2.auth.CommonTlsContext; @@ -39,7 +37,6 @@ import io.envoyproxy.envoy.api.v2.core.GrpcService; import io.envoyproxy.envoy.api.v2.core.GrpcService.GoogleGrpc; import io.envoyproxy.envoy.api.v2.core.HealthStatus; -import io.envoyproxy.envoy.api.v2.core.Node; import io.envoyproxy.envoy.api.v2.core.SelfConfigSource; import io.envoyproxy.envoy.api.v2.core.SocketAddress; import io.envoyproxy.envoy.api.v2.core.TransportSocket; @@ -49,8 +46,11 @@ import io.envoyproxy.envoy.api.v2.route.RouteMatch; import io.envoyproxy.envoy.api.v2.route.VirtualHost; import io.envoyproxy.envoy.config.listener.v2.ApiListener; +import io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest; +import io.envoyproxy.envoy.service.discovery.v3.DiscoveryResponse; import io.envoyproxy.envoy.type.FractionalPercent; import io.envoyproxy.envoy.type.FractionalPercent.DenominatorType; +import io.grpc.xds.EnvoyProtoData.Node; import java.util.List; import javax.annotation.Nullable; @@ -69,6 +69,17 @@ static DiscoveryResponse buildDiscoveryResponse(String versionInfo, .build(); } + static io.envoyproxy.envoy.api.v2.DiscoveryResponse buildDiscoveryResponseV2(String versionInfo, + List resources, String typeUrl, String nonce) { + return + io.envoyproxy.envoy.api.v2.DiscoveryResponse.newBuilder() + .setVersionInfo(versionInfo) + .setTypeUrl(typeUrl) + .addAllResources(resources) + .setNonce(nonce) + .build(); + } + static DiscoveryRequest buildDiscoveryRequest(Node node, String versionInfo, String resourceName, String typeUrl, String nonce) { return buildDiscoveryRequest(node, versionInfo, ImmutableList.of(resourceName), typeUrl, nonce); @@ -79,7 +90,25 @@ static DiscoveryRequest buildDiscoveryRequest(Node node, String versionInfo, return DiscoveryRequest.newBuilder() .setVersionInfo(versionInfo) - .setNode(node) + .setNode(node.toEnvoyProtoNode()) + .setTypeUrl(typeUrl) + .addAllResourceNames(resourceNames) + .setResponseNonce(nonce) + .build(); + } + + static io.envoyproxy.envoy.api.v2.DiscoveryRequest buildDiscoveryRequestV2( + Node node, String versionInfo, String resourceName, String typeUrl, String nonce) { + return buildDiscoveryRequestV2( + node, versionInfo, ImmutableList.of(resourceName), typeUrl, nonce); + } + + static io.envoyproxy.envoy.api.v2.DiscoveryRequest buildDiscoveryRequestV2( + Node node, String versionInfo, List resourceNames, String typeUrl, String nonce) { + return + io.envoyproxy.envoy.api.v2.DiscoveryRequest.newBuilder() + .setVersionInfo(versionInfo) + .setNode(node.toEnvoyProtoNodeV2()) .setTypeUrl(typeUrl) .addAllResourceNames(resourceNames) .setResponseNonce(nonce) diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverIntegrationTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverIntegrationTest.java index fdacff15e98..7007f74ecb8 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverIntegrationTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverIntegrationTest.java @@ -17,7 +17,7 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; -import static io.grpc.xds.XdsClientTestHelper.buildDiscoveryResponse; +import static io.grpc.xds.XdsClientTestHelper.buildDiscoveryResponseV2; import static io.grpc.xds.XdsClientTestHelper.buildListener; import static io.grpc.xds.XdsClientTestHelper.buildRouteConfiguration; import static io.grpc.xds.XdsClientTestHelper.buildVirtualHost; @@ -37,7 +37,6 @@ import io.envoyproxy.envoy.api.v2.DiscoveryResponse; import io.envoyproxy.envoy.api.v2.core.AggregatedConfigSource; import io.envoyproxy.envoy.api.v2.core.ConfigSource; -import io.envoyproxy.envoy.api.v2.core.Node; import io.envoyproxy.envoy.api.v2.route.Route; import io.envoyproxy.envoy.api.v2.route.RouteAction; import io.envoyproxy.envoy.api.v2.route.RouteMatch; @@ -65,6 +64,8 @@ import io.grpc.stub.StreamObserver; import io.grpc.testing.GrpcCleanupRule; import io.grpc.xds.Bootstrapper.ServerInfo; +import io.grpc.xds.EnvoyProtoData.Node; +import io.grpc.xds.XdsClient.XdsChannel; import io.grpc.xds.XdsClient.XdsChannelFactory; import java.io.IOException; import java.util.ArrayDeque; @@ -159,18 +160,16 @@ public StreamObserver streamAggregatedResources( channelFactory = new XdsChannelFactory() { @Override - ManagedChannel createChannel(List servers) { + XdsChannel createChannel(List servers) { assertThat(Iterables.getOnlyElement(servers).getServerUri()).isEqualTo(serverName); - return channel; + return new XdsChannel(channel, false); } }; Bootstrapper bootstrapper = new Bootstrapper() { @Override public BootstrapInfo readBootstrap() { List serverList = - ImmutableList.of( - new ServerInfo(serverName, - ImmutableList.of())); + ImmutableList.of(new ServerInfo(serverName, ImmutableList.of(), null)); return new BootstrapInfo(serverList, FAKE_BOOTSTRAP_NODE); } }; @@ -397,7 +396,7 @@ public void resolve_xdsRoutingLoadBalancing() { List listeners = ImmutableList.of(Any.pack(buildListener(AUTHORITY, Any.pack(httpConnectionManager)))); responseObserver.onNext( - buildDiscoveryResponse("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000")); + buildDiscoveryResponseV2("0", listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, "0000")); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); @@ -479,7 +478,7 @@ public void resolve_weightedTargetLoadBalancing() { buildVirtualHostForRoutes( AUTHORITY, ImmutableList.of(weightedClustersDefaultRoute)))))); responseObserver.onNext( - buildDiscoveryResponse("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0000")); + buildDiscoveryResponseV2("0", routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, "0000")); verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); @@ -548,7 +547,8 @@ private static DiscoveryResponse buildLdsResponseForCluster( ImmutableList.of(host), // exact match clusterName)))) .build())))); - return buildDiscoveryResponse(versionInfo, listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, nonce); + return buildDiscoveryResponseV2( + versionInfo, listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, nonce); } /** @@ -570,7 +570,8 @@ private static DiscoveryResponse buildLdsResponseForRdsResource( Any.pack( buildListener( host, Any.pack(HttpConnectionManager.newBuilder().setRds(rdsConfig).build())))); - return buildDiscoveryResponse(versionInfo, listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, nonce); + return buildDiscoveryResponseV2( + versionInfo, listeners, XdsClientImpl.ADS_TYPE_URL_LDS_V2, nonce); } /** @@ -589,7 +590,7 @@ private static DiscoveryResponse buildRdsResponseForCluster( routeConfigName, ImmutableList.of( buildVirtualHost(ImmutableList.of(host), clusterName))))); - return buildDiscoveryResponse( + return buildDiscoveryResponseV2( versionInfo, routeConfigs, XdsClientImpl.ADS_TYPE_URL_RDS_V2, nonce); } diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 584a53a8f6f..16ce37a6e51 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -83,7 +83,7 @@ public void generateXdsRoutingRawConfig() { new RouteMatch( new PathMatcher(null, "", null), Collections.emptyList(), new FractionMatcher(10, 20)), - new RouteAction("cluster-foo", null)); + new RouteAction(15L, "cluster-foo", null)); Route r2 = new Route( new RouteMatch( @@ -92,6 +92,7 @@ public void generateXdsRoutingRawConfig() { new HeaderMatcher(":scheme", "https", null, null, null, null, null, false)), null), new RouteAction( + 15L, null, Arrays.asList( new ClusterWeight("cluster-foo", 20), @@ -134,7 +135,7 @@ public void generateXdsRoutingRawConfig_allowDuplicateMatchers() { new RouteMatch( new PathMatcher("/service/method", null, null), Collections.emptyList(), null), - new RouteAction("cluster-foo", null)); + new RouteAction(15L, "cluster-foo", null)); Map config = XdsNameResolver.generateXdsRoutingRawConfig(Arrays.asList(route, route)); diff --git a/xds/third_party/envoy/import.sh b/xds/third_party/envoy/import.sh index e528611d171..cd16468c50b 100755 --- a/xds/third_party/envoy/import.sh +++ b/xds/third_party/envoy/import.sh @@ -117,6 +117,8 @@ envoy/extensions/transport_sockets/tls/v3/secret.proto envoy/extensions/transport_sockets/tls/v3/tls.proto envoy/service/discovery/v2/ads.proto envoy/service/discovery/v2/sds.proto +envoy/service/discovery/v3/ads.proto +envoy/service/discovery/v3/discovery.proto envoy/service/load_stats/v2/lrs.proto envoy/type/http.proto envoy/type/matcher/regex.proto diff --git a/xds/third_party/envoy/src/main/proto/envoy/service/discovery/v3/ads.proto b/xds/third_party/envoy/src/main/proto/envoy/service/discovery/v3/ads.proto new file mode 100644 index 00000000000..03021559ab6 --- /dev/null +++ b/xds/third_party/envoy/src/main/proto/envoy/service/discovery/v3/ads.proto @@ -0,0 +1,44 @@ +syntax = "proto3"; + +package envoy.service.discovery.v3; + +import "envoy/service/discovery/v3/discovery.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; + +option java_package = "io.envoyproxy.envoy.service.discovery.v3"; +option java_outer_classname = "AdsProto"; +option java_multiple_files = true; +option java_generic_services = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Aggregated Discovery Service (ADS)] + +// [#not-implemented-hide:] Discovery services for endpoints, clusters, routes, +// and listeners are retained in the package `envoy.api.v2` for backwards +// compatibility with existing management servers. New development in discovery +// services should proceed in the package `envoy.service.discovery.v2`. + +// See https://github.com/lyft/envoy-api#apis for a description of the role of +// ADS and how it is intended to be used by a management server. ADS requests +// have the same structure as their singleton xDS counterparts, but can +// multiplex many resource types on a single stream. The type_url in the +// DiscoveryRequest/DiscoveryResponse provides sufficient information to recover +// the multiplexed singleton APIs at the Envoy instance and management server. +service AggregatedDiscoveryService { + // This is a gRPC-only API. + rpc StreamAggregatedResources(stream DiscoveryRequest) returns (stream DiscoveryResponse) { + } + + rpc DeltaAggregatedResources(stream DeltaDiscoveryRequest) + returns (stream DeltaDiscoveryResponse) { + } +} + +// [#not-implemented-hide:] Not configuration. Workaround c++ protobuf issue with importing +// services: https://github.com/google/protobuf/issues/4221 +message AdsDummy { + option (udpa.annotations.versioning).previous_message_type = + "envoy.service.discovery.v2.AdsDummy"; +} diff --git a/xds/third_party/envoy/src/main/proto/envoy/service/discovery/v3/discovery.proto b/xds/third_party/envoy/src/main/proto/envoy/service/discovery/v3/discovery.proto new file mode 100644 index 00000000000..b8e31160a88 --- /dev/null +++ b/xds/third_party/envoy/src/main/proto/envoy/service/discovery/v3/discovery.proto @@ -0,0 +1,244 @@ +syntax = "proto3"; + +package envoy.service.discovery.v3; + +import "envoy/config/core/v3/base.proto"; + +import "google/protobuf/any.proto"; +import "google/rpc/status.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; + +option java_package = "io.envoyproxy.envoy.service.discovery.v3"; +option java_outer_classname = "DiscoveryProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Common discovery API components] + +// A DiscoveryRequest requests a set of versioned resources of the same type for +// a given Envoy node on some API. +// [#next-free-field: 7] +message DiscoveryRequest { + option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.DiscoveryRequest"; + + // The version_info provided in the request messages will be the version_info + // received with the most recent successfully processed response or empty on + // the first request. It is expected that no new request is sent after a + // response is received until the Envoy instance is ready to ACK/NACK the new + // configuration. ACK/NACK takes place by returning the new API config version + // as applied or the previous API config version respectively. Each type_url + // (see below) has an independent version associated with it. + string version_info = 1; + + // The node making the request. + config.core.v3.Node node = 2; + + // List of resources to subscribe to, e.g. list of cluster names or a route + // configuration name. If this is empty, all resources for the API are + // returned. LDS/CDS may have empty resource_names, which will cause all + // resources for the Envoy instance to be returned. The LDS and CDS responses + // will then imply a number of resources that need to be fetched via EDS/RDS, + // which will be explicitly enumerated in resource_names. + repeated string resource_names = 3; + + // Type of the resource that is being requested, e.g. + // "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment". This is implicit + // in requests made via singleton xDS APIs such as CDS, LDS, etc. but is + // required for ADS. + string type_url = 4; + + // nonce corresponding to DiscoveryResponse being ACK/NACKed. See above + // discussion on version_info and the DiscoveryResponse nonce comment. This + // may be empty only if 1) this is a non-persistent-stream xDS such as HTTP, + // or 2) the client has not yet accepted an update in this xDS stream (unlike + // delta, where it is populated only for new explicit ACKs). + string response_nonce = 5; + + // This is populated when the previous :ref:`DiscoveryResponse ` + // failed to update configuration. The *message* field in *error_details* provides the Envoy + // internal exception related to the failure. It is only intended for consumption during manual + // debugging, the string provided is not guaranteed to be stable across Envoy versions. + google.rpc.Status error_detail = 6; +} + +// [#next-free-field: 7] +message DiscoveryResponse { + option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.DiscoveryResponse"; + + // The version of the response data. + string version_info = 1; + + // The response resources. These resources are typed and depend on the API being called. + repeated google.protobuf.Any resources = 2; + + // [#not-implemented-hide:] + // Canary is used to support two Envoy command line flags: + // + // * --terminate-on-canary-transition-failure. When set, Envoy is able to + // terminate if it detects that configuration is stuck at canary. Consider + // this example sequence of updates: + // - Management server applies a canary config successfully. + // - Management server rolls back to a production config. + // - Envoy rejects the new production config. + // Since there is no sensible way to continue receiving configuration + // updates, Envoy will then terminate and apply production config from a + // clean slate. + // * --dry-run-canary. When set, a canary response will never be applied, only + // validated via a dry run. + bool canary = 3; + + // Type URL for resources. Identifies the xDS API when muxing over ADS. + // Must be consistent with the type_url in the 'resources' repeated Any (if non-empty). + string type_url = 4; + + // For gRPC based subscriptions, the nonce provides a way to explicitly ack a + // specific DiscoveryResponse in a following DiscoveryRequest. Additional + // messages may have been sent by Envoy to the management server for the + // previous version on the stream prior to this DiscoveryResponse, that were + // unprocessed at response send time. The nonce allows the management server + // to ignore any further DiscoveryRequests for the previous version until a + // DiscoveryRequest bearing the nonce. The nonce is optional and is not + // required for non-stream based xDS implementations. + string nonce = 5; + + // [#not-implemented-hide:] + // The control plane instance that sent the response. + config.core.v3.ControlPlane control_plane = 6; +} + +// DeltaDiscoveryRequest and DeltaDiscoveryResponse are used in a new gRPC +// endpoint for Delta xDS. +// +// With Delta xDS, the DeltaDiscoveryResponses do not need to include a full +// snapshot of the tracked resources. Instead, DeltaDiscoveryResponses are a +// diff to the state of a xDS client. +// In Delta XDS there are per-resource versions, which allow tracking state at +// the resource granularity. +// An xDS Delta session is always in the context of a gRPC bidirectional +// stream. This allows the xDS server to keep track of the state of xDS clients +// connected to it. +// +// In Delta xDS the nonce field is required and used to pair +// DeltaDiscoveryResponse to a DeltaDiscoveryRequest ACK or NACK. +// Optionally, a response message level system_version_info is present for +// debugging purposes only. +// +// DeltaDiscoveryRequest plays two independent roles. Any DeltaDiscoveryRequest +// can be either or both of: [1] informing the server of what resources the +// client has gained/lost interest in (using resource_names_subscribe and +// resource_names_unsubscribe), or [2] (N)ACKing an earlier resource update from +// the server (using response_nonce, with presence of error_detail making it a NACK). +// Additionally, the first message (for a given type_url) of a reconnected gRPC stream +// has a third role: informing the server of the resources (and their versions) +// that the client already possesses, using the initial_resource_versions field. +// +// As with state-of-the-world, when multiple resource types are multiplexed (ADS), +// all requests/acknowledgments/updates are logically walled off by type_url: +// a Cluster ACK exists in a completely separate world from a prior Route NACK. +// In particular, initial_resource_versions being sent at the "start" of every +// gRPC stream actually entails a message for each type_url, each with its own +// initial_resource_versions. +// [#next-free-field: 8] +message DeltaDiscoveryRequest { + option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.DeltaDiscoveryRequest"; + + // The node making the request. + config.core.v3.Node node = 1; + + // Type of the resource that is being requested, e.g. + // "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment". + string type_url = 2; + + // DeltaDiscoveryRequests allow the client to add or remove individual + // resources to the set of tracked resources in the context of a stream. + // All resource names in the resource_names_subscribe list are added to the + // set of tracked resources and all resource names in the resource_names_unsubscribe + // list are removed from the set of tracked resources. + // + // *Unlike* state-of-the-world xDS, an empty resource_names_subscribe or + // resource_names_unsubscribe list simply means that no resources are to be + // added or removed to the resource list. + // *Like* state-of-the-world xDS, the server must send updates for all tracked + // resources, but can also send updates for resources the client has not subscribed to. + // + // NOTE: the server must respond with all resources listed in resource_names_subscribe, + // even if it believes the client has the most recent version of them. The reason: + // the client may have dropped them, but then regained interest before it had a chance + // to send the unsubscribe message. See DeltaSubscriptionStateTest.RemoveThenAdd. + // + // These two fields can be set in any DeltaDiscoveryRequest, including ACKs + // and initial_resource_versions. + // + // A list of Resource names to add to the list of tracked resources. + repeated string resource_names_subscribe = 3; + + // A list of Resource names to remove from the list of tracked resources. + repeated string resource_names_unsubscribe = 4; + + // Informs the server of the versions of the resources the xDS client knows of, to enable the + // client to continue the same logical xDS session even in the face of gRPC stream reconnection. + // It will not be populated: [1] in the very first stream of a session, since the client will + // not yet have any resources, [2] in any message after the first in a stream (for a given + // type_url), since the server will already be correctly tracking the client's state. + // (In ADS, the first message *of each type_url* of a reconnected stream populates this map.) + // The map's keys are names of xDS resources known to the xDS client. + // The map's values are opaque resource versions. + map initial_resource_versions = 5; + + // When the DeltaDiscoveryRequest is a ACK or NACK message in response + // to a previous DeltaDiscoveryResponse, the response_nonce must be the + // nonce in the DeltaDiscoveryResponse. + // Otherwise (unlike in DiscoveryRequest) response_nonce must be omitted. + string response_nonce = 6; + + // This is populated when the previous :ref:`DiscoveryResponse ` + // failed to update configuration. The *message* field in *error_details* + // provides the Envoy internal exception related to the failure. + google.rpc.Status error_detail = 7; +} + +// [#next-free-field: 7] +message DeltaDiscoveryResponse { + option (udpa.annotations.versioning).previous_message_type = + "envoy.api.v2.DeltaDiscoveryResponse"; + + // The version of the response data (used for debugging). + string system_version_info = 1; + + // The response resources. These are typed resources, whose types must match + // the type_url field. + repeated Resource resources = 2; + + // field id 3 IS available! + + // Type URL for resources. Identifies the xDS API when muxing over ADS. + // Must be consistent with the type_url in the Any within 'resources' if 'resources' is non-empty. + string type_url = 4; + + // Resources names of resources that have be deleted and to be removed from the xDS Client. + // Removed resources for missing resources can be ignored. + repeated string removed_resources = 6; + + // The nonce provides a way for DeltaDiscoveryRequests to uniquely + // reference a DeltaDiscoveryResponse when (N)ACKing. The nonce is required. + string nonce = 5; +} + +message Resource { + option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.Resource"; + + // The resource's name, to distinguish it from others of the same type of resource. + string name = 3; + + // The aliases are a list of other names that this resource can go by. + repeated string aliases = 4; + + // The resource level version. It allows xDS to track the state of individual + // resources. + string version = 1; + + // The resource being tracked. + google.protobuf.Any resource = 2; +}